diff --git a/server/cmd/mmctl/commands/mmctl_test.go b/server/cmd/mmctl/commands/mmctl_test.go index 24fa2b6d374..c16e7cfdb41 100644 --- a/server/cmd/mmctl/commands/mmctl_test.go +++ b/server/cmd/mmctl/commands/mmctl_test.go @@ -15,7 +15,6 @@ import ( "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/mocks" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -81,8 +80,6 @@ func (s *MmctlE2ETestSuite) SetupMessageExportTestHelper() *api4.TestHelper { jobs.DefaultWatcherPollingInterval = 100 s.th = api4.SetupEnterprise(s.T()).InitBasic() s.th.App.Srv().SetLicense(model.NewTestLicense("message_export")) - messageExportImpl := message_export.MessageExportJobInterfaceImpl{Server: s.th.App.Srv()} - s.th.App.Srv().Jobs.RegisterJobType(model.JobTypeMessageExport, messageExportImpl.MakeWorker(), messageExportImpl.MakeScheduler()) s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MessageExportSettings.DownloadExportResults = true *cfg.MessageExportSettings.EnableExport = true diff --git a/server/enterprise/external_imports.go b/server/enterprise/external_imports.go index a3127fb0587..09223ecef19 100644 --- a/server/enterprise/external_imports.go +++ b/server/enterprise/external_imports.go @@ -36,4 +36,12 @@ import ( _ "github.com/mattermost/enterprise/outgoing_oauth_connections" // Needed to ensure the init() method in the EE gets run _ "github.com/mattermost/enterprise/access_control" + // Needed to ensure the init() method in the EE gets run + _ "github.com/mattermost/enterprise/message_export" + // Needed to ensure the init() method in the EE gets run + _ "github.com/mattermost/enterprise/message_export/actiance_export" + // Needed to ensure the init() method in the EE gets run + _ "github.com/mattermost/enterprise/message_export/csv_export" + // Needed to ensure the init() method in the EE gets run + _ "github.com/mattermost/enterprise/message_export/global_relay_export" ) diff --git a/server/enterprise/internal/file/utils.go b/server/enterprise/internal/file/utils.go deleted file mode 100644 index 48ed3770db7..00000000000 --- a/server/enterprise/internal/file/utils.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package file - -import ( - "os" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// DeleteTemp removes a file and logs the error -// Intended to be called in a defer after the creation of a temp file to ensure cleanup -func DeleteTemp(logger mlog.LoggerIFace, file *os.File) { - err := file.Close() - if err != nil { - logger.Warn("Failed to close temporary file", mlog.String("filename", file.Name()), mlog.Err(err)) - } - err = os.Remove(file.Name()) - if err != nil { - logger.Warn("Failed to delete temporary file", mlog.String("filename", file.Name()), mlog.Err(err)) - } -} diff --git a/server/enterprise/local_imports.go b/server/enterprise/local_imports.go index 449f0a03a5d..7f0ddac8443 100644 --- a/server/enterprise/local_imports.go +++ b/server/enterprise/local_imports.go @@ -9,13 +9,5 @@ import ( // Needed to ensure the init() method in the EE gets run _ "github.com/mattermost/mattermost/server/v8/enterprise/metrics" // Needed to ensure the init() method in the EE gets run - _ "github.com/mattermost/mattermost/server/v8/enterprise/message_export" - // Needed to ensure the init() method in the EE gets run - _ "github.com/mattermost/mattermost/server/v8/enterprise/message_export/actiance_export" - // Needed to ensure the init() method in the EE gets run - _ "github.com/mattermost/mattermost/server/v8/enterprise/message_export/csv_export" - // Needed to ensure the init() method in the EE gets run - _ "github.com/mattermost/mattermost/server/v8/enterprise/message_export/global_relay_export" - // Needed to ensure the init() method in the EE gets run _ "github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch" ) diff --git a/server/enterprise/message_export/actiance_export/actiance_export.go b/server/enterprise/message_export/actiance_export/actiance_export.go deleted file mode 100644 index e29e0b6a273..00000000000 --- a/server/enterprise/message_export/actiance_export/actiance_export.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package actiance_export - -import ( - "archive/zip" - "bytes" - "encoding/xml" - "fmt" - "io" - "os" - "slices" - "strings" - "time" - - "github.com/mattermost/mattermost/server/v8/enterprise/internal/file" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -const ( - XMLNS = "http://www.w3.org/2001/XMLSchema-instance" - ActianceExportFilename = "actiance_export.xml" - ActianceWarningFilename = "warning.txt" - EnsureLastMessageInASort = "zzzzzzzzzzzzzzzzzzzzzzzzzz" -) - -// The root-level element of an actiance export -type RootNode struct { - XMLName xml.Name `xml:"FileDump"` - XMLNS string `xml:"xmlns:xsi,attr"` // this should default to "http://www.w3.org/2001/XMLSchema-instance" - Channels []ChannelExport // one element per channel (open or invite-only), group message, or direct message -} - -// The Conversation element indicates an ad hoc IM conversation or a group chat room. -// The messages from a persistent chat room are exported once a day so that a Conversation entry contains the messages posted to a chat room from 12:00:00 AM to 11:59:59 PM -type ChannelExport struct { - XMLName xml.Name `xml:"Conversation"` - Perspective string `xml:"Perspective,attr"` // the value of this attribute doesn't seem to matter. Using the channel name makes the export more human readable - ChannelId string `xml:"-"` // the unique id of the channel - RoomId string `xml:"RoomID"` - StartTime int64 `xml:"StartTimeUTC"` // utc timestamp (seconds), start of export period or create time of channel, whichever is greater. Example: 1366611728. - JoinEvents []JoinExport // start with a list of all users who were present in the channel during the export period - Elements []Sortable - LeaveEvents []LeaveExport // finish with a list of all users who were present in the channel during the export period - EndTime int64 `xml:"EndTimeUTC"` // utc timestamp (seconds), end of export period or delete time of channel, whichever is lesser. Example: 1366611728. -} - -// The ParticipantEntered element indicates each user who participates in a conversation. -// For chat rooms, there must be one ParticipantEntered element for each user present in the chat room at the beginning of the reporting period -type JoinExport struct { - XMLName xml.Name `xml:"ParticipantEntered"` - UserEmail string `xml:"LoginName"` // the email of the person that joined the channel - UserType string `xml:"UserType"` // the type of the user that joined the channel - JoinTime int64 `xml:"DateTimeUTC"` // utc timestamp (seconds), time at which the user joined. Example: 1366611728 - CorporateEmailID string `xml:"CorporateEmailID"` -} - -// The ParticipantLeft element indicates the user who leaves an active IM or chat room conversation. -// For chat rooms, there must be one ParticipantLeft element for each user present in the chat room at the end of the reporting period. -type LeaveExport struct { - XMLName xml.Name `xml:"ParticipantLeft"` - UserEmail string `xml:"LoginName"` // the email of the person that left the channel - UserType string `xml:"UserType"` // the type of the user that left the channel - LeaveTime int64 `xml:"DateTimeUTC"` // utc timestamp (seconds), time at which the user left. Example: 1366611728 - CorporateEmailID string `xml:"CorporateEmailID"` -} - -type Sortable interface { - SortVal() (int64, string) -} - -func SortableSort(a, b Sortable) int { - aTimestamp, aId := a.SortVal() - bTimestamp, bId := b.SortVal() - if aTimestamp == bTimestamp { - return strings.Compare(aId, bId) - } - return int(aTimestamp - bTimestamp) -} - -// The Message element indicates the message sent by a user -type PostExport struct { - XMLName xml.Name `xml:"Message"` - MessageId string `xml:"MessageId"` // the message id in the db - UserEmail string `xml:"LoginName"` // the email of the person that sent the post - UserType shared.UserType `xml:"UserType"` // the type of the person that sent the post: "user" or "bot" - CreateAt int64 `xml:"DateTimeUTC"` // utc timestamp (unix milliseconds), the post's createAt - - // Allows us to differentiate between: - // - "EditedOriginalMsg": the newly created message (new Id), which holds the pre-edited message contents. The - // "EditedNewMsgId" field will point to the message (original Id) which has the post-edited message content. - // - "EditedNewMsg": the post-edited message content. This is confusing, so be careful: in the db, this EditedNewMsg - // is actually the original messageId because we wanted an edited message to have the same messageId as the - // pre-edited message. But for the purposes of exporting and to keep the mental model clear for end-users, we are - // calling this the EditedNewMsg and EditedNewMsgId, because this will hold the NEW post-edited message contents, - // and that's what's important to the end-user viewing the export. - // - "UpdatedNoMsgChange": the message content hasn't changed, but the post was updated for some reason (reaction, - // replied-to, a reply was edited, a reply was deleted (as of 10.2), perhaps other reasons) - // - "Deleted": the message was deleted. - // - "FileDeleted": this message is recording that a file was deleted. - UpdatedType shared.PostUpdatedType `xml:"UpdatedType,omitempty"` - UpdateAt int64 `xml:"UpdatedDateTimeUTC,omitempty"` // if this is an updated post, this is the updated time (same as deleted time for deleted posts). - - // when a message is edited, the EditedOriginalMsg points to the message Id that now has the newly edited message. - EditedNewMsgId string `xml:"EditedNewMsgId,omitempty"` - - Message string `xml:"Content"` // the text body of the post - PreviewsPost string `xml:"PreviewsPost,omitempty"` // the post id of the post that is previewed by the permalink preview feature -} - -func (p PostExport) SortVal() (int64, string) { - // updated messages are sorted by UpdateAt - if p.UpdatedType != "" { - return p.UpdateAt, p.MessageId - } - return p.CreateAt, p.MessageId -} - -// The FileTransferStarted element indicates the beginning of a file transfer in a conversation -type FileUploadStartExport struct { - XMLName xml.Name `xml:"FileTransferStarted"` - UserEmail string `xml:"LoginName"` // the email of the person that sent the file - UploadStartTime int64 `xml:"DateTimeUTC"` // utc timestamp (seconds), time at which the user started the upload. Example: 1366611728 - Filename string `xml:"UserFileName"` // the name of the file that was uploaded - FilePath string `xml:"FileName"` // the path to the file, as stored on the server -} - -func (f FileUploadStartExport) SortVal() (int64, string) { - // file messages should come after the message they were with - return f.UploadStartTime, EnsureLastMessageInASort -} - -// The FileTransferEnded element indicates the end of a file transfer in a conversation -type FileUploadStopExport struct { - XMLName xml.Name `xml:"FileTransferEnded"` - UserEmail string `xml:"LoginName"` // the email of the person that sent the file - UploadStopTime int64 `xml:"DateTimeUTC"` // utc timestamp (seconds), time at which the user finished the upload. Example: 1366611728 - Filename string `xml:"UserFileName"` // the name of the file that was uploaded - FilePath string `xml:"FileName"` // the path to the file, as stored on the server - Status string `xml:"Status"` // set to either "Completed" or "Failed" depending on the outcome of the upload operation -} - -func (f FileUploadStopExport) SortVal() (int64, string) { - // file messages should come after the message they were with - return f.UploadStopTime, EnsureLastMessageInASort -} - -func ActianceExport(rctx request.CTX, p shared.ExportParams) (shared.RunExportResults, error) { - start := time.Now() - - // Build the channel exports for the channels that had post or user join/leave activity this batch. - exportData, err := shared.GetGenericExportData(p) - if err != nil { - return exportData.Results, err - } - var allUploadedFiles []*model.FileInfo - - // Convert the generic shared.ChannelExports to the Actiance-specific ChannelExports data. - channelExports := make([]ChannelExport, 0, len(exportData.Exports)) - for _, channel := range exportData.Exports { - joinEvents := make([]JoinExport, 0, len(channel.JoinEvents)) - leaveEvents := make([]LeaveExport, 0, len(channel.LeaveEvents)) - - for _, j := range channel.JoinEvents { - joinEvents = append(joinEvents, JoinExport{ - UserEmail: j.UserEmail, - UserType: string(j.UserType), - JoinTime: j.JoinTime, - CorporateEmailID: j.UserEmail, - }) - } - for _, l := range channel.LeaveEvents { - leaveEvents = append(leaveEvents, LeaveExport{ - UserEmail: l.UserEmail, - UserType: string(l.UserType), - LeaveTime: l.LeaveTime, - CorporateEmailID: l.UserEmail, - }) - } - - elements := make([]Sortable, 0, len(channel.Posts)+len(channel.DeletedFiles)+len(channel.UploadStarts)+len(channel.UploadStops)) - for _, p := range channel.Posts { - elements = append(elements, PostExport{ - MessageId: *p.PostId, - UserEmail: *p.UserEmail, - UserType: p.UserType, - CreateAt: *p.PostCreateAt, - UpdatedType: p.UpdatedType, - UpdateAt: p.UpdateAt, - EditedNewMsgId: p.EditedNewMsgId, - Message: p.Message, - PreviewsPost: p.PreviewsPost, - }) - } - for _, p := range channel.DeletedFiles { - elements = append(elements, PostExport{ - MessageId: *p.PostId, - UserEmail: *p.UserEmail, - UserType: p.UserType, - CreateAt: *p.PostCreateAt, - UpdatedType: p.UpdatedType, - UpdateAt: p.UpdateAt, - EditedNewMsgId: p.EditedNewMsgId, - Message: p.Message, - PreviewsPost: p.PreviewsPost, - }) - } - for _, u := range channel.UploadStarts { - elements = append(elements, FileUploadStartExport{ - UserEmail: u.UserEmail, - UploadStartTime: u.UploadStartTime, - Filename: u.FileInfo.Name, - FilePath: u.FileInfo.Path, - }) - } - for _, u := range channel.UploadStops { - elements = append(elements, FileUploadStopExport{ - UserEmail: u.UserEmail, - UploadStopTime: u.UploadStopTime, - Filename: u.FileInfo.Name, - FilePath: u.FileInfo.Path, - Status: u.Status, - }) - } - - // We need to sort all the elements by (updateAt, messageId) because they were added by type above. - slices.SortStableFunc(elements, SortableSort) - - channelExports = append(channelExports, ChannelExport{ - Perspective: channel.DisplayName, - ChannelId: channel.ChannelId, - RoomId: fmt.Sprintf("%v - %v - %v", shared.ChannelTypeDisplayName(channel.ChannelType), - channel.ChannelName, channel.ChannelId), - StartTime: channel.StartTime, - JoinEvents: joinEvents, - Elements: elements, - LeaveEvents: leaveEvents, - EndTime: channel.EndTime, - }) - - allUploadedFiles = append(allUploadedFiles, channel.Files...) - } - - export := &RootNode{ - XMLNS: XMLNS, - Channels: channelExports, - } - - results := exportData.Results - results.ProcessingPostsMs = time.Since(start).Milliseconds() - - results.WriteExportResult, err = writeExport(rctx, export, allUploadedFiles, p.ExportBackend, p.FileAttachmentBackend, p.BatchPath) - results.NumChannels = len(channelExports) - return results, err -} - -func writeExport(rctx request.CTX, export *RootNode, uploadedFiles []*model.FileInfo, exportBackend filestore.FileBackend, fileAttachmentBackend filestore.FileBackend, batchPath string) (res shared.WriteExportResult, err error) { - start := time.Now() - // marshal the export object to xml - xmlData := &bytes.Buffer{} - xmlData.WriteString(xml.Header) - - enc := xml.NewEncoder(xmlData) - enc.Indent("", " ") - if err = enc.Encode(export); err != nil { - return res, fmt.Errorf("unable to convert export to XML: %w", err) - } - if err = enc.Flush(); err != nil { - return res, fmt.Errorf("unable to flush the XML encoder: %w", err) - } - - // Write this batch to a tmp zip, then copy the zip to the export directory. - // Using a 2M buffer because the file backend may be s3 and this optimizes speed and - // memory usage, see: https://github.com/mattermost/mattermost/pull/26629 - buf := make([]byte, 1024*1024*2) - temp, err := os.CreateTemp("", "compliance-export-batch-*.zip") - if err != nil { - return res, fmt.Errorf("unable to create the batch temporary file: %w", err) - } - defer file.DeleteTemp(rctx.Logger(), temp) - - zipFile := zip.NewWriter(temp) - w, err := zipFile.Create(ActianceExportFilename) - if err != nil { - return res, fmt.Errorf("unable to create the xml file in the zipFile created with the batch temporary file: %w", err) - } - if _, err = io.CopyBuffer(w, xmlData, buf); err != nil { - return res, fmt.Errorf("unable to write into the zipFile created with the batch temporary file: %w", err) - } - res.ProcessingXmlMs = time.Since(start).Milliseconds() - - start = time.Now() - - var missingFiles []string - for _, fileInfo := range uploadedFiles { - var attachmentReader io.ReadCloser - attachmentReader, err = fileAttachmentBackend.Reader(fileInfo.Path) - if err != nil { - missingFiles = append(missingFiles, "Warning:"+shared.MissingFileMessageDuringBackendRead+" - "+fileInfo.Path) - rctx.Logger().Warn(shared.MissingFileMessageDuringBackendRead, - mlog.String("filename", fileInfo.Path), - mlog.Err(err), - ) - continue - } - - // There could be many uploadedFiles, so be careful about closing readers. - if err = func() error { - defer attachmentReader.Close() - var zipWriter io.Writer - zipWriter, err = zipFile.Create(fileInfo.Path) - if err != nil { - return err - } - - if _, err = io.CopyBuffer(zipWriter, attachmentReader, buf); err != nil { - return err - } - - return nil - }(); err != nil { - // s3 only errors _here_ if the object key wasn't found. So to handle that: if there is a read - // error (even for local), let's add a warning instead of failing the export. - // Failing the export would fail the entire export run, and every future run would also fail on - // this non-existent file -- not good. - missingFiles = append(missingFiles, "Warning:"+shared.MissingFileMessageDuringCopy+" - "+fileInfo.Path) - rctx.Logger().Warn(shared.MissingFileMessageDuringCopy, - mlog.String("filename", fileInfo.Path), - mlog.Err(err), - ) - } - } - - res.TransferringFilesMs = time.Since(start).Milliseconds() - res.NumWarnings = len(missingFiles) - if res.NumWarnings > 0 { - var w io.Writer - w, err = zipFile.Create(ActianceWarningFilename) - if err != nil { - return res, fmt.Errorf("unable to create the warning file in the zipFile created with the batch temporary file: %w", err) - } - r := strings.NewReader(strings.Join(missingFiles, "\n")) - if _, err = io.CopyBuffer(w, r, buf); err != nil { - return res, fmt.Errorf("unable to write into the zipFile created with the batch temporary file: %w", err) - } - } - - if err = zipFile.Close(); err != nil { - return res, fmt.Errorf("unable to close the zipFile created with the batch temporary file: %w", err) - } - - _, err = temp.Seek(0, io.SeekStart) - if err != nil { - return res, fmt.Errorf("unable to seek to the beginning of the the batch temporary file: %w", err) - } - - start = time.Now() - - // Try to write the file without a timeout due to the potential size of the file. - _, err = filestore.TryWriteFileContext(rctx.Context(), exportBackend, temp, batchPath) - if err != nil { - return res, fmt.Errorf("unable to transfer the batch zip to the file backend: %w", err) - } - - res.TransferringZipMs = time.Since(start).Milliseconds() - - return res, nil -} diff --git a/server/enterprise/message_export/actiance_export/actiance_export_test.go b/server/enterprise/message_export/actiance_export/actiance_export_test.go deleted file mode 100644 index 86a4ee99282..00000000000 --- a/server/enterprise/message_export/actiance_export/actiance_export_test.go +++ /dev/null @@ -1,2336 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package actiance_export - -import ( - "archive/zip" - "bytes" - "encoding/xml" - "fmt" - "io" - "os" - "path" - "strings" - "testing" - - "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -type MyReporter struct { - mock.Mock -} - -func (mr *MyReporter) ReportProgressMessage(message string) { - mr.Called(message) -} - -func TestActianceExport(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - runTestActianceExport(t, fileBackend, fileBackend) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(attachmentTempDir) - assert.NoError(t, err) - }) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - - runTestActianceExport(t, exportBackend, attachmentBackend) - }) -} - -func runTestActianceExport(t *testing.T, exportBackend filestore.FileBackend, attachmentBackend filestore.FileBackend) { - rctx := request.TestContext(t) - rctx = rctx.WithT(i18n.IdentityTfunc()).(*request.Context) - - chanTypeDirect := model.ChannelTypeDirect - actianceExportTests := []struct { - name string - jobEndTime int64 - activity []string - channels model.ChannelList - cmhs map[string][]*model.ChannelMemberHistoryResult - posts []*model.MessageExport - attachments map[string][]*model.FileInfo - expectedData string - expectedFiles int - }{ - { - name: "empty", - jobEndTime: 30, - cmhs: map[string][]*model.ChannelMemberHistoryResult{}, - posts: []*model.MessageExport{}, - attachments: map[string][]*model.FileInfo{}, - expectedData: strings.Join([]string{ - xml.Header, - "", - }, ""), - activity: []string{}, - expectedFiles: 2, - }, - { - name: "posts", - jobEndTime: 500, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - {JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "testname", LeaveTime: model.NewPointer(int64(400))}, - {JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2name", LeaveTime: model.NewPointer(int64(80))}, - {JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3name"}, - {JoinTime: 10, ChannelId: "channel-id", UserId: "test_bot", UserEmail: "test_bot@email", Username: "test_botname", IsBot: true, LeaveTime: model.NewPointer(int64(20))}, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostEditAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("edited message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostDeleteAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("original message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - // deleted post - { - PostId: model.NewPointer("post-id2"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(4)), - PostDeleteAt: model.NewPointer(int64(4)), - PostMessage: model.NewPointer("message2"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - { - PostId: model.NewPointer("post-id3"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message3"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test_bot@email\n", - " bot\n", - " 10\n", - " test_bot@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " post-id2\n", - " test@test.com\n", - " user\n", - " 1\n", - " message2\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " EditedOriginalMsg\n", - " 2\n", - " post-original-id\n", - " original message\n", - " \n", - " \n", - " post-original-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " EditedNewMsg\n", - " 2\n", - " edited message\n", - " \n", - " \n", - " post-id2\n", - " test@test.com\n", - " user\n", - " 1\n", - " Deleted\n", - " 4\n", - " delete message2\n", - " \n", - " \n", - " post-id3\n", - " test@test.com\n", - " user\n", - " 100\n", - " message3\n", - " \n", - " \n", - " test_bot@email\n", - " bot\n", - " 20\n", - " test_bot@email\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 500\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 500\n", - " test@test.com\n", - " \n", - " 500\n", - " \n", - "", - }, ""), - expectedFiles: 2, - }, - { - name: "post with permalink preview", - jobEndTime: 600, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - {JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "test", LeaveTime: model.NewPointer(int64(400))}, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer(`{"previewed_post":"n4w39mc1ff8y5fite4b8hacy1w"}`), - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer(`{"disable_group_highlight":true}`), - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " n4w39mc1ff8y5fite4b8hacy1w\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 100\n", - " message\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 600\n", - " test@test.com\n", - " \n", - " 600\n", - " \n", - "", - }, ""), - expectedFiles: 0, - }, - { - name: "posts with attachments", - jobEndTime: 700, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3", - }, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostOriginalId: model.NewPointer("post-original-id"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - }, - }, - }, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " Completed\n", - " \n", - " \n", - " post-id-2\n", - " test@test.com\n", - " user\n", - " 100\n", - " message\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 700\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 700\n", - " test@test.com\n", - " \n", - " 700\n", - " \n", - "", - }, ""), - expectedFiles: 3, - }, - - { - name: "posts with deleted attachments, no deleted post, and at different time from original post", - jobEndTime: 700, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3", - }, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - UpdateAt: 2, - DeleteAt: 2, - }, - }, - }, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " message1\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " Completed\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " FileDeleted\n", - " 2\n", - " delete test1-attachment\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 700\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 700\n", - " test@test.com\n", - " \n", - " 700\n", - " \n", - "", - }, ""), - expectedFiles: 3, - }, - { - name: "posts with deleted attachments and deleted post", - jobEndTime: 700, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3", - }, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostDeleteAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("message1"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - UpdateAt: 2, - DeleteAt: 2, - }, - }, - }, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " message1\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " \n", - " \n", - " test@test.com\n", - " 1\n", - " test1-attachment\n", - " test1-attachment\n", - " Completed\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " Deleted\n", - " 2\n", - " delete message1\n", - " \n", - " \n", - " post-id-1\n", - " test@test.com\n", - " user\n", - " 1\n", - " FileDeleted\n", - " 2\n", - " delete test1-attachment\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 700\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 700\n", - " test@test.com\n", - " \n", - " 700\n", - " \n", - "", - }, ""), - expectedFiles: 3, - }, - { - name: "joins and leaves after last post, one batch, and post from bot", - jobEndTime: 500, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - {JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "testname", LeaveTime: model.NewPointer(int64(400))}, - {JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2name", LeaveTime: model.NewPointer(int64(80))}, - {JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3name"}, - {JoinTime: 450, ChannelId: "channel-id", UserId: "test4", UserEmail: "test4@email", Username: "test4name", LeaveTime: model.NewPointer(int64(460))}, - {JoinTime: 10, ChannelId: "channel-id", UserId: "test-bot", UserEmail: "test-bot@email", Username: "test-botname", IsBot: true, LeaveTime: model.NewPointer(int64(20))}, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id2"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostDeleteAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("edit message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id3"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(4)), - PostDeleteAt: model.NewPointer(int64(4)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - { - PostId: model.NewPointer("post-id5"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(20)), - PostUpdateAt: model.NewPointer(int64(20)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test-bot@email"), - UserId: model.NewPointer("test-bot"), - Username: model.NewPointer("test-botname"), - IsBot: true, - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id4"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedData: strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test-bot@email\n", - " bot\n", - " 10\n", - " test-bot@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " test4@email\n", - " user\n", - " 450\n", - " test4@email\n", - " \n", - " \n", - " post-id1\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " post-id3\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " post-id2\n", - " test@test.com\n", - " user\n", - " 1\n", - " UpdatedNoMsgChange\n", - " 2\n", - " edit message\n", - " \n", - " \n", - " post-id3\n", - " test@test.com\n", - " user\n", - " 1\n", - " Deleted\n", - " 4\n", - " delete message\n", - " \n", - " \n", - " post-id5\n", - " test-bot@email\n", - " bot\n", - " 20\n", - " message\n", - " \n", - " \n", - " post-id4\n", - " test@test.com\n", - " user\n", - " 100\n", - " message\n", - " \n", - " \n", - " test-bot@email\n", - " bot\n", - " 20\n", - " test-bot@email\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test4@email\n", - " user\n", - " 460\n", - " test4@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 500\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 500\n", - " test@test.com\n", - " \n", - " 500\n", - " \n", - "", - }, ""), - expectedFiles: 2, - }, - } - - for _, tt := range actianceExportTests { - t.Run(tt.name, func(t *testing.T) { - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - if len(tt.attachments) > 0 { - for post_id, attachments := range tt.attachments { - call := mockStore.FileInfoStore.On("GetForPost", post_id, true, true, false) - call.Run(func(args mock.Arguments) { - call.Return(attachments, nil) - }) - _, err := attachmentBackend.WriteFile(bytes.NewReader([]byte{}), attachments[0].Path) - require.NoError(t, err) - - t.Cleanup(func() { - err = attachmentBackend.RemoveFile(attachments[0].Path) - require.NoError(t, err) - }) - } - } - - if len(tt.cmhs) > 0 { - for channelId, cmhs := range tt.cmhs { - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", int64(1), tt.jobEndTime, []string{channelId}). - Return(cmhs, nil) - } - } - - if tt.activity != nil { - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return(tt.activity, nil) - } - - if tt.channels != nil { - mockStore.ChannelStore.On("GetMany", tt.activity, true). - Return(tt.channels, nil) - } - - myMockReporter := MyReporter{} - defer myMockReporter.AssertExpectations(t) - if len(tt.activity) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.channel_message") - } - if len(tt.cmhs) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.activity_message") - } - - channelMetadata, channelMemberHistories, err := shared.CalculateChannelExports(rctx, - shared.ChannelExportsParams{ - Store: shared.NewMessageExportStore(mockStore), - ExportPeriodStartTime: 1, - ExportPeriodEndTime: tt.jobEndTime, - ChannelBatchSize: 100, - ChannelHistoryBatchSize: 100, - ReportProgressMessage: myMockReporter.ReportProgressMessage, - }) - assert.NoError(t, err) - - exportFileName := path.Join("export", "jobName", "jobName-batch001.zip") - res, err := ActianceExport(rctx, shared.ExportParams{ - ChannelMetadata: channelMetadata, - Posts: tt.posts, - ChannelMemberHistories: channelMemberHistories, - BatchPath: exportFileName, - BatchStartTime: 1, - BatchEndTime: tt.jobEndTime, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - }) - assert.NoError(t, err) - assert.Equal(t, 0, res.NumWarnings) - - zipBytes, err := exportBackend.ReadFile(exportFileName) - assert.NoError(t, err) - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - actiancexml, err := zipReader.File[0].Open() - require.NoError(t, err) - defer actiancexml.Close() - xmlData, err := io.ReadAll(actiancexml) - assert.NoError(t, err) - - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><> actual:\n%s\n", string(xmlData)) - assert.Equal(t, tt.expectedData, string(xmlData)) - - t.Cleanup(func() { - err = exportBackend.RemoveFile(exportFileName) - assert.NoError(t, err) - }) - }) - } -} - -func TestActianceExportMultipleBatches(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - runTestActianceExportMultipleBatches(t, fileBackend, fileBackend) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(attachmentTempDir) - assert.NoError(t, err) - }) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - - runTestActianceExportMultipleBatches(t, exportBackend, attachmentBackend) - }) -} - -func runTestActianceExportMultipleBatches(t *testing.T, exportBackend filestore.FileBackend, attachmentBackend filestore.FileBackend) { - rctx := request.TestContext(t) - rctx = rctx.WithT(i18n.IdentityTfunc()).(*request.Context) - - chanTypeDirect := model.ChannelTypeDirect - actianceMultiBatchExportTests := []struct { - name string - jobStartTime int64 - jobEndTime int64 - numBatches int - activity []string - channels model.ChannelList - cmhs map[string][]*model.ChannelMemberHistoryResult - posts [][]*model.MessageExport - attachments map[string][]*model.FileInfo - expectedData []string - expectedFiles int - }{ - { - name: "joins and leaves after last post, and before second batch, two batches", - jobStartTime: 1, - jobEndTime: 500, - numBatches: 2, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - // will be included in both batches: - {JoinTime: 0, ChannelId: "channel-id", UserId: "test", UserEmail: "test@email", Username: "testname", LeaveTime: model.NewPointer(int64(400))}, - // Only first batch: - {JoinTime: 2, ChannelId: "channel-id", UserId: "testA", UserEmail: "testA@email", Username: "testAname", LeaveTime: model.NewPointer(int64(3))}, - // Only second batch: - {JoinTime: 8, ChannelId: "channel-id", UserId: "test2", UserEmail: "test2@email", Username: "test2name", LeaveTime: model.NewPointer(int64(80))}, - {JoinTime: 10, ChannelId: "channel-id", UserId: "test_bot", UserEmail: "test_bot@email", Username: "test_botname", IsBot: true, LeaveTime: model.NewPointer(int64(20))}, - {JoinTime: 400, ChannelId: "channel-id", UserId: "test3", UserEmail: "test3@email", Username: "test3name"}, - {JoinTime: 450, ChannelId: "channel-id", UserId: "test4", UserEmail: "test4@email", Username: "test4name", LeaveTime: model.NewPointer(int64(460))}, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: [][]*model.MessageExport{ - { - { - PostId: model.NewPointer("post-id1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id2"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(4)), - PostDeleteAt: model.NewPointer(int64(4)), - PostMessage: model.NewPointer("edit message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - { - { - PostId: model.NewPointer("post-id3"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(5)), - PostDeleteAt: model.NewPointer(int64(5)), - PostMessage: model.NewPointer("message2"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - { - PostId: model.NewPointer("post-id4"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message3"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }}, - attachments: map[string][]*model.FileInfo{}, - expectedData: []string{ - strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 1\n", - " test@test.com\n", - " \n", - " \n", - " testA@email\n", - " user\n", - " 2\n", - " testA@email\n", - " \n", - " \n", - " post-id1\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " post-id2\n", - " test@test.com\n", - " user\n", - " 1\n", - " EditedOriginalMsg\n", - " 4\n", - " post-original-id\n", - " edit message\n", - " \n", - " \n", - " testA@email\n", - " user\n", - " 3\n", - " testA@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 4\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 4\n", - " test@test.com\n", - " \n", - " 4\n", - " \n", - "", - }, ""), - strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 4\n", - " \n", - " test@email\n", - " user\n", - " 0\n", - " test@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 4\n", - " test@test.com\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 8\n", - " test2@email\n", - " \n", - " \n", - " test_bot@email\n", - " bot\n", - " 10\n", - " test_bot@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 400\n", - " test3@email\n", - " \n", - " \n", - " test4@email\n", - " user\n", - " 450\n", - " test4@email\n", - " \n", - " \n", - " post-id3\n", - " test@test.com\n", - " user\n", - " 1\n", - " message2\n", - " \n", - " \n", - " post-id3\n", - " test@test.com\n", - " user\n", - " 1\n", - " Deleted\n", - " 5\n", - " delete message2\n", - " \n", - " \n", - " post-id4\n", - " test@test.com\n", - " user\n", - " 100\n", - " message3\n", - " \n", - " \n", - " test_bot@email\n", - " bot\n", - " 20\n", - " test_bot@email\n", - " \n", - " \n", - " test2@email\n", - " user\n", - " 80\n", - " test2@email\n", - " \n", - " \n", - " test@email\n", - " user\n", - " 400\n", - " test@email\n", - " \n", - " \n", - " test4@email\n", - " user\n", - " 460\n", - " test4@email\n", - " \n", - " \n", - " test3@email\n", - " user\n", - " 500\n", - " test3@email\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 500\n", - " test@test.com\n", - " \n", - " 500\n", - " \n", - "", - }, ""), - }, - expectedFiles: 2, - }, - } - - for _, tt := range actianceMultiBatchExportTests { - t.Run(tt.name, func(t *testing.T) { - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - if len(tt.attachments) > 0 { - for post_id, attachments := range tt.attachments { - call := mockStore.FileInfoStore.On("GetForPost", post_id, true, true, false) - call.Run(func(args mock.Arguments) { - call.Return(attachments, nil) - }) - _, err := attachmentBackend.WriteFile(bytes.NewReader([]byte{}), attachments[0].Path) - require.NoError(t, err) - - t.Cleanup(func() { - err = attachmentBackend.RemoveFile(attachments[0].Path) - require.NoError(t, err) - }) - } - } - - if len(tt.cmhs) > 0 { - for channelId, cmhs := range tt.cmhs { - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", int64(1), tt.jobEndTime, []string{channelId}). - Return(cmhs, nil) - } - } - - if tt.activity != nil { - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return(tt.activity, nil) - } - - if tt.channels != nil { - mockStore.ChannelStore.On("GetMany", tt.activity, true). - Return(tt.channels, nil) - } - - myMockReporter := MyReporter{} - defer myMockReporter.AssertExpectations(t) - if len(tt.activity) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.channel_message") - } - if len(tt.cmhs) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.activity_message") - } - - channelMetadata, channelMemberHistories, err := shared.CalculateChannelExports(rctx, - shared.ChannelExportsParams{ - Store: shared.NewMessageExportStore(mockStore), - ExportPeriodStartTime: 1, - ExportPeriodEndTime: tt.jobEndTime, - ChannelBatchSize: 100, - ChannelHistoryBatchSize: 100, - ReportProgressMessage: myMockReporter.ReportProgressMessage, - }) - assert.NoError(t, err) - - batchStartTime := int64(1) - - for batch := 0; batch < tt.numBatches; batch++ { - var batchEndTime int64 - if batch == tt.numBatches-1 { - batchEndTime = tt.jobEndTime - } else { - batchEndTime = *tt.posts[batch][len(tt.posts[batch])-1].PostUpdateAt - } - exportFileName := path.Join("export", "jobName", - fmt.Sprintf("jobName-batch00%d.zip", batch+1)) - - res, err := ActianceExport(rctx, shared.ExportParams{ - ChannelMetadata: channelMetadata, - Posts: tt.posts[batch], - ChannelMemberHistories: channelMemberHistories, - BatchPath: exportFileName, - BatchStartTime: batchStartTime, - BatchEndTime: batchEndTime, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - }) - assert.NoError(t, err) - assert.Equal(t, 0, res.NumWarnings) - - zipBytes, err := exportBackend.ReadFile(exportFileName) - assert.NoError(t, err) - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - actiancexml, err := zipReader.File[0].Open() - require.NoError(t, err) - xmlData, err := io.ReadAll(actiancexml) - actiancexml.Close() - assert.NoError(t, err) - - assert.Equal(t, tt.expectedData[batch], string(xmlData), fmt.Sprintf("batch %v", batch)) - - batchStartTime = *tt.posts[batch][len(tt.posts[batch])-1].PostUpdateAt - - t.Cleanup(func() { - err = exportBackend.RemoveFile(exportFileName) - assert.NoError(t, err) - }) - } - }) - } -} - -func TestMultipleActianceExport(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - runTestMultipleActianceExport(t, fileBackend, fileBackend) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(attachmentTempDir) - assert.NoError(t, err) - }) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - - runTestMultipleActianceExport(t, exportBackend, attachmentBackend) - }) -} - -func runTestMultipleActianceExport(t *testing.T, exportBackend filestore.FileBackend, attachmentBackend filestore.FileBackend) { - rctx := request.TestContext(t) - rctx = rctx.WithT(i18n.IdentityTfunc()).(*request.Context) - - chanTypeDirect := model.ChannelTypeDirect - multActianceExportTests := []struct { - name string - jobEndTime int64 - activity []string - channels model.ChannelList - cmhs map[string][]*model.ChannelMemberHistoryResult - posts map[string][]*model.MessageExport - attachments map[string][]*model.FileInfo - expectedData map[string]string - expectedFiles int - }{ - { - name: "post,export,delete,export", - jobEndTime: 500, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - {JoinTime: 0, ChannelId: "channel-id", UserId: "user-id", UserEmail: "test@test.com", Username: "username", LeaveTime: model.NewPointer(int64(400))}, - }, - }, - posts: map[string][]*model.MessageExport{ - "step1": { - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - "step2": { - { - PostId: model.NewPointer("post-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostDeleteAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - }, - }, - expectedData: map[string]string{ - "step1": strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@test.com\n", - " user\n", - " 0\n", - " test@test.com\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 400\n", - " test@test.com\n", - " \n", - " 500\n", - " \n", - "", - }, ""), - // We're redoing the export completely, so we'll get the original message then the deleted record - "step2": strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@test.com\n", - " user\n", - " 0\n", - " test@test.com\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " post-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " Deleted\n", - " 2\n", - " delete message\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 400\n", - " test@test.com\n", - " \n", - " 500\n", - " \n", - "", - }, ""), - }, - expectedFiles: 2, - }, - { - name: "post,export,edit,export", - jobEndTime: 600, - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - {JoinTime: 0, ChannelId: "channel-id", UserId: "user-id", UserEmail: "test@test.com", Username: "username", LeaveTime: model.NewPointer(int64(450))}, - }, - }, - activity: []string{"channel-id"}, - channels: model.ChannelList{{ - TeamId: "team-id", - Id: "channel-id", - Name: "channel-name", - DisplayName: "channel-display-name", - Type: model.ChannelTypeDirect, - }}, - posts: map[string][]*model.MessageExport{ - "step1": { - { - PostId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - "step2": { - // new post which holds the original message contents - { - PostId: model.NewPointer("post-id-new"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostDeleteAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - // old post which has been edited - { - PostId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(2)), - PostEditAt: model.NewPointer(int64(2)), - PostMessage: model.NewPointer("edit message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - }, - expectedData: map[string]string{ - "step1": strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@test.com\n", - " user\n", - " 0\n", - " test@test.com\n", - " \n", - " \n", - " post-original-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " message\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 450\n", - " test@test.com\n", - " \n", - " 600\n", - " \n", - "", - }, ""), - // We're redoing the export completely, so we'll get the original message then the deleted record - "step2": strings.Join([]string{ - xml.Header, - "\n", - " \n", - " direct - channel-name - channel-id\n", - " 1\n", - " \n", - " test@test.com\n", - " user\n", - " 0\n", - " test@test.com\n", - " \n", - " \n", - " post-id-new\n", - " test@test.com\n", - " user\n", - " 1\n", - " EditedOriginalMsg\n", - " 2\n", - " post-original-id\n", - " message\n", - " \n", - " \n", - " post-original-id\n", - " test@test.com\n", - " user\n", - " 1\n", - " EditedNewMsg\n", - " 2\n", - " edit message\n", - " \n", - " \n", - " test@test.com\n", - " user\n", - " 450\n", - " test@test.com\n", - " \n", - " 600\n", - " \n", - "", - }, ""), - }, - expectedFiles: 2, - }, - } - - for _, tt := range multActianceExportTests { - t.Run(tt.name, func(t *testing.T) { - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - if len(tt.cmhs) > 0 { - for channelId, cmhs := range tt.cmhs { - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", int64(1), tt.jobEndTime, []string{channelId}). - Return(cmhs, nil) - } - } - - if tt.activity != nil { - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return(tt.activity, nil) - } - - if tt.channels != nil { - mockStore.ChannelStore.On("GetMany", tt.activity, true). - Return(tt.channels, nil) - } - - myMockReporter := MyReporter{} - defer myMockReporter.AssertExpectations(t) - if len(tt.activity) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.channel_message") - } - if len(tt.cmhs) > 0 { - myMockReporter.On("ReportProgressMessage", "ent.message_export.actiance_export.calculate_channel_exports.activity_message") - } - - channelMetadata, channelMemberHistories, err := shared.CalculateChannelExports(rctx, - shared.ChannelExportsParams{ - Store: shared.NewMessageExportStore(mockStore), - ExportPeriodStartTime: 1, - ExportPeriodEndTime: tt.jobEndTime, - ChannelBatchSize: 100, - ChannelHistoryBatchSize: 100, - ReportProgressMessage: myMockReporter.ReportProgressMessage, - }) - assert.NoError(t, err) - - exportFileName := path.Join("export", "jobName", "jobName-batch001.zip") - res, err := ActianceExport(rctx, shared.ExportParams{ - ChannelMetadata: channelMetadata, - Posts: tt.posts["step1"], - ChannelMemberHistories: channelMemberHistories, - BatchPath: exportFileName, - BatchStartTime: 1, - BatchEndTime: tt.jobEndTime, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - }) - - assert.NoError(t, err) - assert.Equal(t, 0, res.NumWarnings) - - zipBytes, err := exportBackend.ReadFile(exportFileName) - assert.NoError(t, err) - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - actiancexml, err := zipReader.File[0].Open() - require.NoError(t, err) - defer actiancexml.Close() - xmlData, err := io.ReadAll(actiancexml) - assert.NoError(t, err) - - assert.Equal(t, tt.expectedData["step1"], string(xmlData)) - - res, err = ActianceExport(rctx, shared.ExportParams{ - ChannelMetadata: channelMetadata, - Posts: tt.posts["step2"], - ChannelMemberHistories: channelMemberHistories, - BatchPath: exportFileName, - BatchStartTime: 1, - BatchEndTime: tt.jobEndTime, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - }) - assert.NoError(t, err) - assert.Equal(t, 0, res.NumWarnings) - - zipBytes, err = exportBackend.ReadFile(exportFileName) - assert.NoError(t, err) - zipReader, err = zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - actiancexml, err = zipReader.File[0].Open() - require.NoError(t, err) - defer actiancexml.Close() - xmlData, err = io.ReadAll(actiancexml) - assert.NoError(t, err) - - assert.Equal(t, tt.expectedData["step2"], string(xmlData)) - - t.Cleanup(func() { - err = exportBackend.RemoveFile(exportFileName) - assert.NoError(t, err) - }) - }) - } -} - -func TestWriteExportWarnings(t *testing.T) { - tempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(tempDir) - assert.NoError(t, err) - }) - - config := filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: tempDir, - } - - fileBackend, err := filestore.NewFileBackend(config) - assert.NoError(t, err) - - rctx := request.TestContext(t) - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - // Do not create the files, we want them to error - uploadedFiles := []*model.FileInfo{ - {Name: "test", Id: "12345", Path: "missing.txt"}, - {Name: "test2", Id: "54321", Path: "missing.txt"}, - } - export := &RootNode{ - XMLNS: XMLNS, - Channels: []ChannelExport{}, - } - - exportFileName := path.Join("export", "jobName", "jobName-batch001.zip") - res, err := writeExport(rctx, export, uploadedFiles, fileBackend, fileBackend, exportFileName) - assert.NoError(t, err) - assert.Equal(t, 2, res.NumWarnings) - - err = fileBackend.RemoveFile(exportFileName) - require.NoError(t, err) -} - -func Test_channelHasActivity(t *testing.T) { - tests := []struct { - name string - cmhs []*model.ChannelMemberHistoryResult - startTime int64 - endTime int64 - want bool - }{ - { - name: "no activity", - cmhs: nil, - startTime: 1000, - endTime: 2000, - want: false, - }, - { - name: "no activity in bounds (but activity out of bounds)", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 900, - LeaveTime: nil, - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - { - ChannelId: "channelid", - UserId: "testid2", - JoinTime: 2100, - LeaveTime: nil, - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: false, - }, - { - name: "join on lower bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 1000, - LeaveTime: nil, - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - { - name: "join within bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 1500, - LeaveTime: nil, - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - { - name: "join on upper bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 2000, - LeaveTime: nil, - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - { - name: "leave on lower bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 100, - LeaveTime: model.NewPointer[int64](1000), - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - { - name: "leave within bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 100, - LeaveTime: model.NewPointer[int64](1500), - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - { - name: "leave on upper bounds", - cmhs: []*model.ChannelMemberHistoryResult{ - { - ChannelId: "channelid", - UserId: "testid", - JoinTime: 100, - LeaveTime: model.NewPointer[int64](2000), - UserEmail: "testemail@email.com", - Username: "test_username", - IsBot: false, - UserDeleteAt: 0, - }, - }, - startTime: 1000, - endTime: 2000, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, shared.ChannelHasActivity(tt.cmhs, tt.startTime, tt.endTime), "channelHasActivity(%v, %v, %v)", tt.cmhs, tt.startTime, tt.endTime) - }) - } -} diff --git a/server/enterprise/message_export/actiance_export/main_test.go b/server/enterprise/message_export/actiance_export/main_test.go deleted file mode 100644 index 4275303b85a..00000000000 --- a/server/enterprise/message_export/actiance_export/main_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package actiance_export - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/channels/api4" - "github.com/mattermost/mattermost/server/v8/channels/testlib" -) - -var mainHelper *testlib.MainHelper - -func TestMain(m *testing.M) { - mainHelper = testlib.NewMainHelper() - defer mainHelper.Close() - api4.SetMainHelper(mainHelper) - - mainHelper.Main(m) -} diff --git a/server/enterprise/message_export/actiance_export/test_helpers.go b/server/enterprise/message_export/actiance_export/test_helpers.go deleted file mode 100644 index 79e28a5957c..00000000000 --- a/server/enterprise/message_export/actiance_export/test_helpers.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package actiance_export - -import ( - "encoding/xml" - "io" - "testing" - - "github.com/stretchr/testify/require" -) - -type ChannelExportWithSpecifics struct { - XMLName xml.Name `xml:"Conversation"` - Perspective string `xml:"Perspective,attr"` - ChannelId string `xml:"-"` - RoomId string `xml:"RoomID"` - StartTime int64 `xml:"StartTimeUTC"` - JoinEvents []*JoinExport `xml:"ParticipantEntered"` - Messages []*PostExport `xml:"Message"` - FileStarts []*FileUploadStartExport `xml:"FileTransferStarted"` - FileStops []*FileUploadStopExport `xml:"FileTransferEnded"` - LeaveEvents []*LeaveExport `xml:"ParticipantLeft"` - EndTime int64 `xml:"EndTimeUTC"` -} - -func GetChannelExports(t *testing.T, r io.Reader) []*ChannelExportWithSpecifics { - decoder := xml.NewDecoder(r) - var exportedChannels []*ChannelExportWithSpecifics - for { - token, err := decoder.Token() - if token == nil || err != nil { - break - } - switch se := token.(type) { - case xml.StartElement: - if se.Name.Local == "Conversation" { - var a *ChannelExportWithSpecifics - err = decoder.DecodeElement(&a, &se) - require.NoError(t, err) - exportedChannels = append(exportedChannels, a) - } - default: - } - } - - return exportedChannels -} diff --git a/server/enterprise/message_export/common_export/common_export.go b/server/enterprise/message_export/common_export/common_export.go deleted file mode 100644 index fa31e1d739e..00000000000 --- a/server/enterprise/message_export/common_export/common_export.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package common_export - -import ( - "fmt" - "strconv" - - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" -) - -const MissingFileMessage = "File missing for post; cannot copy file to archive" - -type ChannelMemberJoin struct { - UserId string - IsBot bool - Email string - Username string - Datetime int64 -} - -type ChannelMemberLeave struct { - UserId string - IsBot bool - Email string - Username string - Datetime int64 -} - -type ChannelMember struct { - UserId string - IsBot bool - Email string - Username string -} - -type MetadataChannel struct { - TeamId *string - TeamName *string - TeamDisplayName *string - ChannelId string - ChannelName string - ChannelDisplayName string - ChannelType model.ChannelType - RoomId string - StartTime int64 - EndTime int64 - MessagesCount int - AttachmentsCount int -} - -type Metadata struct { - Channels map[string]*MetadataChannel - MessagesCount int - AttachmentsCount int - StartTime int64 - EndTime int64 -} - -func (metadata *Metadata) Update(post *model.MessageExport, attachments int) { - channelMetadata, ok := metadata.Channels[*post.ChannelId] - if !ok { - channelMetadata = &MetadataChannel{ - TeamId: post.TeamId, - TeamName: post.TeamName, - TeamDisplayName: post.TeamDisplayName, - ChannelId: *post.ChannelId, - ChannelName: *post.ChannelName, - ChannelDisplayName: *post.ChannelDisplayName, - ChannelType: *post.ChannelType, - RoomId: fmt.Sprintf("%v - %v", ChannelTypeDisplayName(*post.ChannelType), *post.ChannelId), - StartTime: *post.PostCreateAt, - MessagesCount: 0, - AttachmentsCount: 0, - } - } - - channelMetadata.EndTime = *post.PostCreateAt - channelMetadata.AttachmentsCount += attachments - metadata.AttachmentsCount += attachments - channelMetadata.MessagesCount += 1 - metadata.MessagesCount += 1 - if metadata.StartTime == 0 { - metadata.StartTime = *post.PostCreateAt - } - metadata.EndTime = *post.PostCreateAt - metadata.Channels[*post.ChannelId] = channelMetadata -} - -type ChannelExportsParams struct { - Store shared.MessageExportStore - ExportPeriodStartTime int64 - ExportPeriodEndTime int64 - ChannelBatchSize int - ChannelHistoryBatchSize int - ReportProgressMessage func(message string) -} - -// CalculateChannelExports returns the channel info ( map[channelId]*MetadataChannel ) and the channel user -// joins/leaves ( map[channelId][]*model.ChannelMemberHistoryResult ) for any channel that has had activity -// (posts or user join/leaves) between ExportPeriodStartTime and ExportPeriodEndTime. -func CalculateChannelExports(rctx request.CTX, opt ChannelExportsParams) (map[string]*MetadataChannel, map[string][]*model.ChannelMemberHistoryResult, error) { - // Which channels had user activity in the export period? - activeChannelIds, err := opt.Store.ChannelMemberHistory().GetChannelsWithActivityDuring(opt.ExportPeriodStartTime, opt.ExportPeriodEndTime) - if err != nil { - return nil, nil, err - } - - if len(activeChannelIds) == 0 { - return nil, nil, nil - } - - rctx.Logger().Debug("Started CalculateChannelExports", mlog.Int("export_period_start_time", opt.ExportPeriodStartTime), mlog.Int("export_period_end_time", opt.ExportPeriodEndTime), mlog.Int("num_active_channel_ids", len(activeChannelIds))) - message := rctx.T("ent.message_export.actiance_export.calculate_channel_exports.channel_message", model.StringMap{"NumChannels": strconv.Itoa(len(activeChannelIds))}) - opt.ReportProgressMessage(message) - - // For each channel, get its metadata. - channelMetadata := make(map[string]*MetadataChannel, len(activeChannelIds)) - - // Use batches to reduce db load and network waste. - for pos := 0; pos < len(activeChannelIds); pos += opt.ChannelBatchSize { - upTo := min(pos+opt.ChannelBatchSize, len(activeChannelIds)) - batch := activeChannelIds[pos:upTo] - channels, err := opt.Store.Channel().GetMany(batch, true) - if err != nil { - return nil, nil, err - } - - for _, channel := range channels { - channelMetadata[channel.Id] = &MetadataChannel{ - TeamId: model.NewPointer(channel.TeamId), - ChannelId: channel.Id, - ChannelName: channel.Name, - ChannelDisplayName: channel.DisplayName, - ChannelType: channel.Type, - RoomId: fmt.Sprintf("%v - %v", ChannelTypeDisplayName(channel.Type), channel.Id), - StartTime: opt.ExportPeriodStartTime, - EndTime: opt.ExportPeriodEndTime, - } - } - } - - historiesByChannelId := make(map[string][]*model.ChannelMemberHistoryResult, len(activeChannelIds)) - - // Now that we have metadata, get channelMemberHistories for each channel. - // Use batches to reduce total db load and network waste. - for pos := 0; pos < len(activeChannelIds); pos += opt.ChannelHistoryBatchSize { - // This may take a while, so update the system console UI. - message := rctx.T("ent.message_export.actiance_export.calculate_channel_exports.activity_message", model.StringMap{ - "NumChannels": strconv.Itoa(len(activeChannelIds)), - "NumCompleted": strconv.Itoa(pos), - }) - opt.ReportProgressMessage(message) - - upTo := min(pos+opt.ChannelHistoryBatchSize, len(activeChannelIds)) - batch := activeChannelIds[pos:upTo] - channelMemberHistories, err := opt.Store.ChannelMemberHistory().GetUsersInChannelDuring(opt.ExportPeriodStartTime, opt.ExportPeriodEndTime, batch) - if err != nil { - return nil, nil, err - } - - // collect the channelMemberHistories by channelId - for _, entry := range channelMemberHistories { - historiesByChannelId[entry.ChannelId] = append(historiesByChannelId[entry.ChannelId], entry) - } - } - - return channelMetadata, historiesByChannelId, nil -} - -func GetJoinsAndLeavesForChannel(startTime int64, endTime int64, channelMembersHistory []*model.ChannelMemberHistoryResult, - postAuthors map[string]ChannelMember) ([]ChannelMemberJoin, []ChannelMemberLeave) { - var joins []ChannelMemberJoin - var leaves []ChannelMemberLeave - - alreadyJoined := make(map[string]bool) - for _, cmh := range channelMembersHistory { - if cmh.UserDeleteAt > 0 && cmh.UserDeleteAt < startTime { - continue - } - - if cmh.JoinTime > endTime { - continue - } - - if cmh.LeaveTime != nil && *cmh.LeaveTime < startTime { - continue - } - - if cmh.JoinTime <= endTime { - joins = append(joins, ChannelMemberJoin{ - UserId: cmh.UserId, - IsBot: cmh.IsBot, - Email: cmh.UserEmail, - Username: cmh.Username, - Datetime: cmh.JoinTime, - }) - alreadyJoined[cmh.UserId] = true - } - - if cmh.LeaveTime != nil && *cmh.LeaveTime <= endTime { - leaves = append(leaves, ChannelMemberLeave{ - UserId: cmh.UserId, - IsBot: cmh.IsBot, - Email: cmh.UserEmail, - Username: cmh.Username, - Datetime: *cmh.LeaveTime, - }) - } - } - - for _, member := range postAuthors { - if alreadyJoined[member.UserId] { - continue - } - - joins = append(joins, ChannelMemberJoin{ - UserId: member.UserId, - IsBot: member.IsBot, - Email: member.Email, - Username: member.Username, - Datetime: startTime, - }) - } - return joins, leaves -} - -func ChannelTypeDisplayName(channelType model.ChannelType) string { - return map[model.ChannelType]string{ - model.ChannelTypeOpen: "public", - model.ChannelTypePrivate: "private", - model.ChannelTypeDirect: "direct", - model.ChannelTypeGroup: "group", - }[channelType] -} diff --git a/server/enterprise/message_export/common_export/common_export_test.go b/server/enterprise/message_export/common_export/common_export_test.go deleted file mode 100644 index 0e6717618e4..00000000000 --- a/server/enterprise/message_export/common_export/common_export_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package common_export - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/public/model" -) - -func TestUpdateMetadata(t *testing.T) { - metadata := Metadata{ - Channels: map[string]*MetadataChannel{}, - MessagesCount: 0, - AttachmentsCount: 0, - StartTime: 0, - EndTime: 0, - } - - testString := "test" - chanTypeDirect := model.ChannelTypeDirect - createdAt := int64(12345) - - post := model.MessageExport{ - TeamId: &testString, - TeamName: &testString, - TeamDisplayName: &testString, - - ChannelId: &testString, - ChannelName: &testString, - ChannelDisplayName: &testString, - ChannelType: &chanTypeDirect, - - UserId: &testString, - UserEmail: &testString, - Username: &testString, - - PostId: &testString, - PostCreateAt: &createdAt, - PostMessage: &testString, - PostType: &testString, - PostOriginalId: &testString, - PostFileIds: []string{}, - } - metadata.Update(&post, 2) - - assert.Len(t, metadata.Channels, 1) - assert.Equal(t, 1, metadata.Channels["test"].MessagesCount) - assert.Equal(t, 2, metadata.Channels["test"].AttachmentsCount) - assert.Equal(t, 1, metadata.MessagesCount) - assert.Equal(t, 2, metadata.AttachmentsCount) - - metadata.Update(&post, 2) - - assert.Len(t, metadata.Channels, 1) - assert.Equal(t, 2, metadata.Channels["test"].MessagesCount) - assert.Equal(t, 4, metadata.Channels["test"].AttachmentsCount) - assert.Equal(t, 2, metadata.MessagesCount) - assert.Equal(t, 4, metadata.AttachmentsCount) - - testString2 := "test2" - post.ChannelId = &testString2 - - metadata.Update(&post, 2) - - assert.Len(t, metadata.Channels, 2) - assert.Equal(t, 2, metadata.Channels["test"].MessagesCount) - assert.Equal(t, 4, metadata.Channels["test"].AttachmentsCount) - assert.Equal(t, 1, metadata.Channels["test2"].MessagesCount) - assert.Equal(t, 2, metadata.Channels["test2"].AttachmentsCount) - assert.Equal(t, 3, metadata.MessagesCount) - assert.Equal(t, 6, metadata.AttachmentsCount) -} - -func TestGetJoinsAndLeavesForChannel(t *testing.T) { - channel := MetadataChannel{ - StartTime: 100, - EndTime: 200, - ChannelId: "good-request-1", - TeamId: model.NewPointer("test"), - TeamName: model.NewPointer("test"), - TeamDisplayName: model.NewPointer("test"), - ChannelName: "test", - ChannelDisplayName: "test", - ChannelType: "O", - } - - tt := []struct { - name string - channel MetadataChannel - membersHistory []*model.ChannelMemberHistoryResult - usersInPosts map[string]ChannelMember - expectedJoins int - expectedLeaves int - }{ - { - name: "no-joins-no-leaves", - channel: channel, - membersHistory: nil, - usersInPosts: nil, - expectedJoins: 0, - expectedLeaves: 0, - }, - { - name: "joins-and-leaves-outside-the-range", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 1, LeaveTime: model.NewPointer(int64(10)), UserId: "test", UserEmail: "test", Username: "test"}, - {JoinTime: 250, LeaveTime: model.NewPointer(int64(260)), UserId: "test", UserEmail: "test", Username: "test"}, - {JoinTime: 300, UserId: "test", UserEmail: "test", Username: "test"}, - }, - usersInPosts: nil, - expectedJoins: 0, - expectedLeaves: 0, - }, - { - name: "join-and-leave-during-the-range", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 100, LeaveTime: model.NewPointer(int64(150)), UserId: "test", UserEmail: "test", Username: "test"}, - }, - usersInPosts: nil, - expectedJoins: 1, - expectedLeaves: 1, - }, - { - name: "join-during-and-leave-after-the-range", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 150, LeaveTime: model.NewPointer(int64(300)), UserId: "test", UserEmail: "test", Username: "test"}, - }, - usersInPosts: nil, - expectedJoins: 1, - expectedLeaves: 0, - }, - { - name: "join-before-and-leave-during-the-range", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 99, LeaveTime: model.NewPointer(int64(150)), UserId: "test", UserEmail: "test", Username: "test"}, - }, - usersInPosts: nil, - expectedJoins: 1, - expectedLeaves: 1, - }, - { - name: "join-before-and-leave-after-the-range", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 99, LeaveTime: model.NewPointer(int64(350)), UserId: "test", UserEmail: "test", Username: "test"}, - }, - usersInPosts: nil, - expectedJoins: 1, - expectedLeaves: 0, - }, - { - name: "implicit-joins", - channel: channel, - membersHistory: nil, - usersInPosts: map[string]ChannelMember{ - "test1": {UserId: "test1", Email: "test1", Username: "test1"}, - "test2": {UserId: "test2", Email: "test2", Username: "test2"}, - }, - expectedJoins: 2, - expectedLeaves: 0, - }, - { - name: "implicit-joins-with-explicit-joins", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test1", UserEmail: "test1", Username: "test1"}, - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test3", UserEmail: "test3", Username: "test3"}, - }, - usersInPosts: map[string]ChannelMember{ - "test1": {UserId: "test1", Email: "test1", Username: "test1"}, - "test2": {UserId: "test2", Email: "test2", Username: "test2"}, - }, - expectedJoins: 3, - expectedLeaves: 2, - }, - { - name: "join-leave-and-join-again", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test1", UserEmail: "test1", Username: "test1"}, - {JoinTime: 160, LeaveTime: model.NewPointer(int64(180)), UserId: "test1", UserEmail: "test1", Username: "test1"}, - }, - usersInPosts: nil, - expectedJoins: 2, - expectedLeaves: 2, - }, - { - name: "deactivated-members-dont-show", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 50}, - {JoinTime: 160, LeaveTime: model.NewPointer(int64(180)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 50}, - }, - usersInPosts: nil, - expectedJoins: 0, - expectedLeaves: 0, - }, - { - name: "deactivated-members-show-if-deleted-after-latest-export", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 150}, - {JoinTime: 160, LeaveTime: model.NewPointer(int64(180)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 150}, - }, - usersInPosts: nil, - expectedJoins: 2, - expectedLeaves: 2, - }, - { - name: "deactivated-members-show-and-dont-show", - channel: channel, - membersHistory: []*model.ChannelMemberHistoryResult{ - {JoinTime: 130, LeaveTime: model.NewPointer(int64(150)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 50}, - {JoinTime: 160, LeaveTime: model.NewPointer(int64(180)), UserId: "test1", UserEmail: "test1", Username: "test1", UserDeleteAt: 150}, - }, - usersInPosts: nil, - expectedJoins: 1, - expectedLeaves: 1, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - joins, leaves := GetJoinsAndLeavesForChannel(tc.channel.StartTime, tc.channel.EndTime, tc.membersHistory, tc.usersInPosts) - assert.Len(t, joins, tc.expectedJoins) - assert.Len(t, leaves, tc.expectedLeaves) - }) - } -} diff --git a/server/enterprise/message_export/csv_export/csv_export.go b/server/enterprise/message_export/csv_export/csv_export.go deleted file mode 100644 index bc20b313139..00000000000 --- a/server/enterprise/message_export/csv_export/csv_export.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package csv_export - -import ( - "archive/zip" - "encoding/csv" - "encoding/json" - "fmt" - "io" - "os" - "path" - "slices" - "strconv" - "strings" - - "github.com/mattermost/mattermost/server/v8/enterprise/internal/file" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -const ( - EnterPostType = "enter" - LeavePostType = "leave" - PreviouslyJoinedPostType = "previously-joined" - CSVWarningFilename = "warning.txt" -) - -type Row struct { - CreateAt int64 - UpdateAt int64 - UpdateType shared.PostUpdatedType - TeamId string - TeamName string - TeamDisplayName string - ChannelId string - ChannelName string - ChannelDisplayName string - ChannelType string - UserId string - UserEmail string - Username string - PostId string - EditedByPostId string - RepliedToPostId string - PostMessage string - PostType string - UserType string - PreviewsPostId string -} - -func CsvExport(rctx request.CTX, p shared.ExportParams) (shared.RunExportResults, error) { - // Build the channel exports for the channels that had post or user join/leave activity this batch. - exportData, err := shared.GetGenericExportData(p) - results := exportData.Results - if err != nil { - return results, err - } - - totalRows := results.CreatedPosts + results.EditedOrigMsgPosts + results.DeletedPosts + results.EditedNewMsgPosts + - results.UpdatedPosts + results.UploadedFiles + results.DeletedFiles + results.Joins - rows := make([]Row, 0, totalRows) - for _, channel := range exportData.Exports { - rows = append(rows, getJoinLeavePosts(channel)...) - for _, p := range channel.Posts { - rows = append(rows, postToRow(p, "message", p.PostCreateAt, p.Message)) - } - - for _, u := range channel.UploadStarts { - rows = append(rows, attachmentToRow(shared.UploadStartToExportEntry(u))) - } - for _, d := range channel.DeletedFiles { - rows = append(rows, attachmentToRow(d)) - } - } - - // We need to sort all the elements by (CreateAt, PostId) because they were added by type and by channel above. - slices.SortStableFunc(rows, func(a, b Row) int { - if a.CreateAt == b.CreateAt { - return strings.Compare(a.PostId, b.PostId) - } - return int(a.CreateAt - b.CreateAt) - }) - - // We've got the data, now its write time: - - // Write this batch to a tmp zip, then copy the zip to the export directory. - // Using a 2M buffer because the file backend may be s3 and this optimizes speed and - // memory usage, see: https://github.com/mattermost/mattermost/pull/26629 - buf := make([]byte, 1024*1024*2) - temp, err := os.CreateTemp("", "compliance-export-batch-*.zip") - if err != nil { - return results, fmt.Errorf("unable to create temporary CSV export file: %w", err) - } - defer file.DeleteTemp(rctx.Logger(), temp) - - zipFile := zip.NewWriter(temp) - csvFile, err := zipFile.Create("posts.csv") - if err != nil { - return results, fmt.Errorf("unable to create the zip export file: %w", err) - } - csvWriter := csv.NewWriter(csvFile) - err = csvWriter.Write([]string{ - "Post Creation Time", - "Post Update Time", - "Post Update Type", - "Team Id", - "Team Name", - "Team Display Name", - "Channel Id", - "Channel Name", - "Channel Display Name", - "Channel Type", - "User Id", - "User Email", - "Username", - "Post Id", - "Edited By Post Id", - "Replied to Post Id", - "Post Message", - "Post Type", - "User Type", - "Previews Post Id", - }) - - if err != nil { - return results, fmt.Errorf("unable to add header to the CSV export: %w", err) - } - - for _, row := range rows { - if err = csvWriter.Write(rowToStringSlice(row)); err != nil { - return results, fmt.Errorf("unable to export a row: %w", err) - } - } - - csvWriter.Flush() - - var missingFiles []string - for _, post := range p.Posts { - var attachments []*model.FileInfo - attachments, err = shared.GetPostAttachments(p.Db, post) - if err != nil { - return results, err - } - - for _, attachment := range attachments { - var r io.ReadCloser - r, err = p.FileAttachmentBackend.Reader(attachment.Path) - if err != nil { - missingFiles = append(missingFiles, "Warning:"+shared.MissingFileMessageDuringBackendRead+" - Post: "+*post.PostId+" - "+attachment.Path) - rctx.Logger().Warn(shared.MissingFileMessageDuringBackendRead, - mlog.String("post_id", *post.PostId), - mlog.String("filename", attachment.Path), - mlog.Err(err), - ) - continue - } - - // Probably don't need to be this careful (see actiance_export.go), but may as well be consistent. - if err = func() error { - defer r.Close() - var attachmentDst io.Writer - attachmentDst, err = zipFile.Create(path.Join("files", *post.PostId, fmt.Sprintf("%s-%s", attachment.Id, path.Base(attachment.Path)))) - if err != nil { - return err - } - - _, err = io.CopyBuffer(attachmentDst, r, buf) - if err != nil { - return err - } - - return nil - }(); err != nil { - // s3 only errors _here_ if the object key wasn't found. So to handle that: if there is a read - // error (even for local), let's add a warning instead of failing the export. - // Failing the export would fail the entire export run, and every future run would also fail on - // this non-existent file -- not good. - missingFiles = append(missingFiles, "Warning:"+shared.MissingFileMessageDuringCopy+" - Post: "+*post.PostId+" - "+attachment.Path) - rctx.Logger().Warn(shared.MissingFileMessageDuringCopy, - mlog.String("post_id", *post.PostId), - mlog.String("filename", attachment.Path), - mlog.Err(err), - ) - } - } - } - - results.NumWarnings = len(missingFiles) - if results.NumWarnings > 0 { - metadataFile, _ := zipFile.Create(CSVWarningFilename) - for _, value := range missingFiles { - _, err = metadataFile.Write([]byte(value + "\n")) - if err != nil { - return results, fmt.Errorf("unable to create the warning file: %w", err) - } - } - } - - metadataFile, err := zipFile.Create("metadata.json") - if err != nil { - return results, fmt.Errorf("unable to create the zip file: %w", err) - } - data, err := json.MarshalIndent(exportData.Metadata, "", " ") - if err != nil { - return results, fmt.Errorf("unable to convert metadata to json: %w", err) - } - _, err = metadataFile.Write(data) - if err != nil { - return results, fmt.Errorf("unable to add metadata file to the zip file: %w", err) - } - err = zipFile.Close() - if err != nil { - return results, fmt.Errorf("unable to close the zip file: %w", err) - } - - _, err = temp.Seek(0, 0) - if err != nil { - return results, fmt.Errorf("unable to seek to start of export file: %w", err) - } - - // Try to write the file without a timeout due to the potential size of the file. - _, err = filestore.TryWriteFileContext(rctx.Context(), p.ExportBackend, temp, p.BatchPath) - if err != nil { - return results, fmt.Errorf("unable to write the csv file: %w", err) - } - return results, nil -} - -func getJoinLeavePosts(channel shared.ChannelExport) []Row { - var joinLeavePosts []Row - - for _, join := range channel.JoinEvents { - enterMessage := fmt.Sprintf("User %s (%s) joined the channel", join.Username, join.UserEmail) - enterPostType := EnterPostType - if join.JoinTime <= channel.StartTime { - enterPostType = PreviouslyJoinedPostType - enterMessage = fmt.Sprintf("User %s (%s) was already in the channel", join.Username, join.UserEmail) - } - joinLeavePosts = append( - joinLeavePosts, - postToRow(shared.PostExport{ - MessageExport: model.MessageExport{ - TeamId: &channel.TeamId, - TeamName: &channel.TeamName, - TeamDisplayName: &channel.TeamDisplayName, - ChannelId: &channel.ChannelId, - ChannelName: &channel.ChannelName, - ChannelDisplayName: &channel.DisplayName, - ChannelType: &channel.ChannelType, - UserId: &join.UserId, - UserEmail: &join.UserEmail, - Username: &join.Username, - IsBot: join.UserType == shared.Bot, - PostId: model.NewPointer(""), - PostCreateAt: &join.JoinTime, - PostMessage: &enterMessage, - PostType: &enterPostType, - PostOriginalId: model.NewPointer(""), - PostFileIds: []string{}, - }, - }, enterPostType, &join.JoinTime, enterMessage), - ) - } - for _, leave := range channel.LeaveEvents { - if leave.ClosedOut { - // csv does not record closed-out leaves; see export_data.go for further explanation. - continue - } - leaveMessage := fmt.Sprintf("User %s (%s) left the channel", leave.Username, leave.UserEmail) - leavePostType := LeavePostType - - joinLeavePosts = append( - joinLeavePosts, - postToRow(shared.PostExport{ - MessageExport: model.MessageExport{ - TeamId: &channel.TeamId, - TeamName: &channel.TeamName, - TeamDisplayName: &channel.TeamDisplayName, - ChannelId: &channel.ChannelId, - ChannelName: &channel.ChannelName, - ChannelDisplayName: &channel.DisplayName, - ChannelType: &channel.ChannelType, - UserId: &leave.UserId, - UserEmail: &leave.UserEmail, - Username: &leave.Username, - IsBot: leave.UserType == shared.Bot, - PostId: model.NewPointer(""), - PostCreateAt: &leave.LeaveTime, - PostMessage: &leaveMessage, - PostType: &leavePostType, - PostOriginalId: model.NewPointer(""), - PostFileIds: []string{}, - }, - }, leavePostType, &leave.LeaveTime, leaveMessage), - ) - } - - return joinLeavePosts -} - -func postToRow(p shared.PostExport, postType string, createTime *int64, message string) Row { - userType := "user" - if p.IsBot { - userType = "bot" - } - return Row{ - CreateAt: model.SafeDereference(createTime), - UpdateAt: model.SafeDereference(p.PostUpdateAt), - UpdateType: p.UpdatedType, - TeamId: model.SafeDereference(p.TeamId), - TeamName: model.SafeDereference(p.TeamName), - TeamDisplayName: model.SafeDereference(p.TeamDisplayName), - ChannelId: model.SafeDereference(p.ChannelId), - ChannelName: model.SafeDereference(p.ChannelName), - ChannelDisplayName: model.SafeDereference(p.ChannelDisplayName), - ChannelType: shared.ChannelTypeDisplayName(model.SafeDereference(p.ChannelType)), - UserId: model.SafeDereference(p.UserId), - UserEmail: model.SafeDereference(p.UserEmail), - Username: model.SafeDereference(p.Username), - PostId: model.SafeDereference(p.PostId), - EditedByPostId: p.EditedNewMsgId, - RepliedToPostId: model.SafeDereference(p.PostRootId), - PostMessage: message, - PostType: postType, - UserType: userType, - PreviewsPostId: p.PreviewID(), - } -} - -func rowToStringSlice(r Row) []string { - return []string{ - strconv.FormatInt(r.CreateAt, 10), - strconv.FormatInt(r.UpdateAt, 10), - string(r.UpdateType), - r.TeamId, - r.TeamName, - r.TeamDisplayName, - r.ChannelId, - r.ChannelName, - r.ChannelDisplayName, - r.ChannelType, - r.UserId, - r.UserEmail, - r.Username, - r.PostId, - r.EditedByPostId, - r.RepliedToPostId, - r.PostMessage, - r.PostType, - r.UserType, - r.PreviewsPostId, - } -} - -func attachmentToRow(post shared.PostExport) Row { - message := strings.TrimSpace(fmt.Sprintf("%s (files/%s/%s-%s)", post.FileInfo.Name, *post.PostId, post.FileInfo.Id, path.Base(post.FileInfo.Path))) - postType := "attachment" - if post.UpdatedType == shared.FileDeleted { - postType = "deleted attachment" - post.PostUpdateAt = model.NewPointer(post.FileInfo.DeleteAt) - } - - return postToRow(post, postType, post.PostCreateAt, message) -} diff --git a/server/enterprise/message_export/csv_export/csv_export_test.go b/server/enterprise/message_export/csv_export/csv_export_test.go deleted file mode 100644 index 0136544044b..00000000000 --- a/server/enterprise/message_export/csv_export/csv_export_test.go +++ /dev/null @@ -1,912 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package csv_export - -import ( - "archive/zip" - "bytes" - "fmt" - "io" - "os" - "path" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -func TestPostToRow(t *testing.T) { - chanTypeDirect := model.ChannelTypeDirect - // these two posts were made in the same channel - post := model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - } - - post_without_team := model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - } - - post_with_other_type := model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostType: model.NewPointer("other"), - } - - post_with_other_type_bot := model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostType: model.NewPointer("other"), - IsBot: true, - } - - post_with_permalink_preview := model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostProps: model.NewPointer(`{"previewed_post":"n4w39mc1ff8y5fite4b8hacy1w"}`), - } - torowtests := []struct { - name string - in model.MessageExport - out Row - }{ - { - "simple row", - post, - Row{1, 1, "", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "message", "message", "user", ""}, - }, - { - "without team data", - post_without_team, - Row{1, 1, "", "", "", "", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "message", "message", "user", ""}, - }, - { - "with special post type", - post_with_other_type, - Row{1, 1, "", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "message", "other", "user", ""}, - }, - { - "with special post type from bot", - post_with_other_type_bot, - Row{1, 1, "", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "message", "other", "bot", ""}, - }, - { - "with permalink preview", - post_with_permalink_preview, - Row{1, 1, "", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "message", "message", "user", "n4w39mc1ff8y5fite4b8hacy1w"}, - }, - } - - for _, tt := range torowtests { - t.Run(tt.name, func(t *testing.T) { - postType := model.SafeDereference(tt.in.PostType) - if postType == "" { - postType = "message" - } - in := shared.PostExport{ - MessageExport: tt.in, - } - assert.Equal(t, tt.out, postToRow(in, postType, tt.in.PostCreateAt, *tt.in.PostMessage)) - }) - } -} - -func TestAttachmentToRow(t *testing.T) { - chanTypeDirect := model.ChannelTypeDirect - post := shared.PostExport{ - MessageExport: model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - }, - FileInfo: &model.FileInfo{ - Name: "test1", - Id: "12345", - Path: "filename.txt", - }, - } - - postDeleted := shared.PostExport{ - MessageExport: model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(10)), - PostDeleteAt: model.NewPointer(int64(10)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostProps: model.NewPointer(`{"deleteBy":"user-id"}`), - }, - UpdatedType: shared.FileDeleted, - FileInfo: &model.FileInfo{ - Name: "test2", - Id: "12346", - Path: "filename.txt", - DeleteAt: 10, - }, - } - - toRowTests := []struct { - name string - post shared.PostExport - out Row - }{ - { - "simple attachment", - post, - Row{1, 1, "", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "test1 (files/post-id/12345-filename.txt)", "attachment", "user", ""}, - }, - { - "simple deleted attachment", - postDeleted, - Row{1, 10, "FileDeleted", "team-id", "team-name", "team-display-name", "channel-id", "channel-name", "channel-display-name", "direct", "user-id", "test@test.com", "username", "post-id", "", "post-root-id", "test2 (files/post-id/12346-filename.txt)", "deleted attachment", "user", ""}, - }, - } - - for _, tt := range toRowTests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.out, attachmentToRow(tt.post)) - }) - } -} - -func TestGetPostAttachments(t *testing.T) { - chanTypeDirect := model.ChannelTypeDirect - post := &model.MessageExport{ - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - } - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - files, err := shared.GetPostAttachments(shared.NewMessageExportStore(mockStore), post) - assert.NoError(t, err) - assert.Empty(t, files) - - post.PostFileIds = []string{"1", "2"} - - mockStore.FileInfoStore.On("GetForPost", *post.PostId, true, true, false).Return([]*model.FileInfo{{Name: "test"}, {Name: "test2"}}, nil) - - files, err = shared.GetPostAttachments(shared.NewMessageExportStore(mockStore), post) - assert.NoError(t, err) - assert.Len(t, files, 2) - - post.PostId = model.NewPointer("post-id-2") - - mockStore.FileInfoStore.On("GetForPost", *post.PostId, true, true, false).Return(nil, model.NewAppError("Test", "test", nil, "", 400)) - - files, err = shared.GetPostAttachments(shared.NewMessageExportStore(mockStore), post) - assert.Error(t, err) - assert.Nil(t, files) -} - -func TestCsvExport(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - runTestCsvExportDedicatedExportFilestore(t, fileBackend, fileBackend) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(exportTempDir) - assert.NoError(t, err) - }) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(attachmentTempDir) - assert.NoError(t, err) - }) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - - runTestCsvExportDedicatedExportFilestore(t, exportBackend, attachmentBackend) - }) -} - -func runTestCsvExportDedicatedExportFilestore(t *testing.T, exportBackend filestore.FileBackend, attachmentBackend filestore.FileBackend) { - rctx := request.TestContext(t) - - header := "Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id\n" - - chanTypeDirect := model.ChannelTypeDirect - csvExportTests := []struct { - name string - cmhs map[string][]*model.ChannelMemberHistoryResult - metadata map[string]*shared.MetadataChannel - startTime int64 - endTime int64 - posts []*model.MessageExport - attachments map[string][]*model.FileInfo - expectedPosts string - expectedMetadata string - expectedFiles int - }{ - { - name: "empty", - cmhs: map[string][]*model.ChannelMemberHistoryResult{}, - posts: []*model.MessageExport{}, - attachments: map[string][]*model.FileInfo{}, - expectedPosts: header, - expectedMetadata: "{\n \"Channels\": null,\n \"MessagesCount\": 0,\n \"AttachmentsCount\": 0,\n \"StartTime\": 0,\n \"EndTime\": 0\n}", - expectedFiles: 2, - }, - { - name: "posts", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test", UserEmail: "test", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer(`{"previewed_post":"o4w39mc1ff8y5fite4b8hacy1x"}`), - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedPosts: strings.Join([]string{ - header, - "0,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test,test,test,,,,User test (test) was already in the channel,previously-joined,user,\n", - "1,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,,,,User username (test@test.com) was already in the channel,previously-joined,user,\n", - "1,1,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,,message,message,user,\n", - "8,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test2,test2,test2,,,,User test2 (test2) joined the channel,enter,user,\n", - "80,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test2,test2,test2,,,,User test2 (test2) left the channel,leave,user,\n", - "100,100,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,post-root-id,message,message,user,\n", - "100,100,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,post-root-id,message,message,user,o4w39mc1ff8y5fite4b8hacy1x\n", - }, ""), - expectedMetadata: "{\n \"Channels\": {\n \"channel-id\": {\n \"TeamId\": \"team-id\",\n \"TeamName\": \"team-name\",\n \"TeamDisplayName\": \"team-display-name\",\n \"ChannelId\": \"channel-id\",\n \"ChannelName\": \"channel-name\",\n \"ChannelDisplayName\": \"channel-display-name\",\n \"ChannelType\": \"D\",\n \"RoomId\": \"direct - channel-id\",\n \"StartTime\": 1,\n \"EndTime\": 100,\n \"MessagesCount\": 3,\n \"AttachmentsCount\": 0\n }\n },\n \"MessagesCount\": 3,\n \"AttachmentsCount\": 0,\n \"StartTime\": 1,\n \"EndTime\": 100\n}", - expectedFiles: 2, - }, - { - name: "deleted post", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test", UserEmail: "test", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100)), - PostUpdateAt: model.NewPointer(int64(101)), - PostDeleteAt: model.NewPointer(int64(101)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedPosts: strings.Join([]string{ - header, - "0,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test,test,test,,,,User test (test) was already in the channel,previously-joined,user,\n", - "1,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,,,,User username (test@test.com) was already in the channel,previously-joined,user,\n", - "1,1,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,,message,message,user,\n", - "100,101,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,,message,message,user,\n", - "100,101,Deleted,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id,,,delete message,message,user,\n", - }, ""), - expectedMetadata: "{\n \"Channels\": {\n \"channel-id\": {\n \"TeamId\": \"team-id\",\n \"TeamName\": \"team-name\",\n \"TeamDisplayName\": \"team-display-name\",\n \"ChannelId\": \"channel-id\",\n \"ChannelName\": \"channel-name\",\n \"ChannelDisplayName\": \"channel-display-name\",\n \"ChannelType\": \"D\",\n \"RoomId\": \"direct - channel-id\",\n \"StartTime\": 1,\n \"EndTime\": 100,\n \"MessagesCount\": 3,\n \"AttachmentsCount\": 0\n }\n },\n \"MessagesCount\": 3,\n \"AttachmentsCount\": 0,\n \"StartTime\": 1,\n \"EndTime\": 100\n}", - expectedFiles: 2, - }, - - { - name: "posts with deleted attachment and deleted post, and at different time from non-deleted original post", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test", UserEmail: "test", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message 1"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-3"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(2)), - PostUpdateAt: model.NewPointer(int64(3)), - PostDeleteAt: model.NewPointer(int64(3)), - PostMessage: model.NewPointer("message 3"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test2"}, - PostProps: model.NewPointer("{\"deleteBy\":\"user-id\"}"), - }, - { - PostId: model.NewPointer("post-id-2"), - PostOriginalId: model.NewPointer(""), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100)), - PostCreateAt: model.NewPointer(int64(100)), - PostMessage: model.NewPointer("message 2"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - // NOTE: this post will be deleted, but the post-id-2 is not deleted. - PostFileIds: []string{"test3"}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1", - Id: "test1", - Path: "test1", - }, - }, - "post-id-3": { - { - Name: "test2", - Id: "test2", - Path: "test2", - DeleteAt: 3, - }, - }, - "post-id-2": { - { - Name: "test3", - Id: "test3", - Path: "test3", - DeleteAt: 102, - }, - }, - }, - expectedPosts: strings.Join([]string{ - header, - "0,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test,test,test,,,,User test (test) was already in the channel,previously-joined,user,\n", - "1,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,,,,User username (test@test.com) was already in the channel,previously-joined,user,\n", - "1,1,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-1,,,message 1,message,user,\n", - "1,1,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-1,,,test1 (files/post-id-1/test1-test1),attachment,user,\n", - "2,3,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-3,,,message 3,message,user,\n", - "2,3,Deleted,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-3,,,delete message 3,message,user,\n", - "2,3,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-3,,,test2 (files/post-id-3/test2-test2),attachment,user,\n", - "2,3,FileDeleted,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-3,,,test2 (files/post-id-3/test2-test2),deleted attachment,user,\n", - "8,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test2,test2,test2,,,,User test2 (test2) joined the channel,enter,user,\n", - "80,0,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,test2,test2,test2,,,,User test2 (test2) left the channel,leave,user,\n", - "100,100,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-2,,post-id-1,message 2,message,user,\n", - "100,100,,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-2,,post-id-1,test3 (files/post-id-2/test3-test3),attachment,user,\n", - "100,102,FileDeleted,team-id,team-name,team-display-name,channel-id,channel-name,channel-display-name,direct,user-id,test@test.com,username,post-id-2,,post-id-1,test3 (files/post-id-2/test3-test3),deleted attachment,user,\n", - }, ""), - expectedMetadata: "{\n \"Channels\": {\n \"channel-id\": {\n \"TeamId\": \"team-id\",\n \"TeamName\": \"team-name\",\n \"TeamDisplayName\": \"team-display-name\",\n \"ChannelId\": \"channel-id\",\n \"ChannelName\": \"channel-name\",\n \"ChannelDisplayName\": \"channel-display-name\",\n \"ChannelType\": \"D\",\n \"RoomId\": \"direct - channel-id\",\n \"StartTime\": 1,\n \"EndTime\": 100,\n \"MessagesCount\": 4,\n \"AttachmentsCount\": 3\n }\n },\n \"MessagesCount\": 4,\n \"AttachmentsCount\": 3,\n \"StartTime\": 1,\n \"EndTime\": 100\n}", - expectedFiles: 5, - }, - } - - for _, tt := range csvExportTests { - t.Run(tt.name, func(t *testing.T) { - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - if len(tt.attachments) > 0 { - for postId, attachments := range tt.attachments { - call := mockStore.FileInfoStore.On("GetForPost", postId, true, true, false) - call.Run(func(args mock.Arguments) { - call.Return(tt.attachments[args.Get(0).(string)], nil) - }) - _, err := attachmentBackend.WriteFile(bytes.NewReader([]byte{}), attachments[0].Path) - require.NoError(t, err) - t.Cleanup(func() { - err = attachmentBackend.RemoveFile(attachments[0].Path) - require.NoError(t, err) - }) - } - } - - exportFileName := path.Join("export", "jobName", "jobName-batch001-csv.zip") - results, err := CsvExport(rctx, shared.ExportParams{ - ChannelMetadata: tt.metadata, - Posts: tt.posts, - ChannelMemberHistories: tt.cmhs, - BatchPath: exportFileName, - BatchStartTime: tt.startTime, - BatchEndTime: tt.endTime, - Config: nil, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - }) - assert.NoError(t, err) - assert.Equal(t, 0, results.NumWarnings) - - zipBytes, err := exportBackend.ReadFile(exportFileName) - assert.NoError(t, err) - t.Cleanup(func() { - err = exportBackend.RemoveFile(exportFileName) - require.NoError(t, err) - }) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - assert.Len(t, zipReader.File, tt.expectedFiles) - - postsFile, err := zipReader.File[0].Open() - require.NoError(t, err) - defer postsFile.Close() - postsFileData, err := io.ReadAll(postsFile) - assert.NoError(t, err) - postsFile.Close() - - metadataFile, err := zipReader.File[len(zipReader.File)-1].Open() - require.NoError(t, err) - defer metadataFile.Close() - metadataFileData, err := io.ReadAll(metadataFile) - require.NoError(t, err) - err = metadataFile.Close() - require.NoError(t, err) - - assert.Equal(t, tt.expectedPosts, string(postsFileData)) - assert.Equal(t, tt.expectedMetadata, string(metadataFileData)) - }) - } -} - -func TestWriteExportWarnings(t *testing.T) { - rctx := request.TestContext(t) - - chanTypeDirect := model.ChannelTypeDirect - cmhs := map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test", UserEmail: "test", Username: "test", LeaveTime: model.NewPointer(int64(400)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2", Username: "test2", LeaveTime: model.NewPointer(int64(80)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3", Username: "test3", - }, - }, - } - metadata := map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - } - - posts := []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"post-id-1"}, - }, - { - PostId: model.NewPointer("post-id-3"), - PostOriginalId: model.NewPointer(""), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(2)), - PostUpdateAt: model.NewPointer(int64(3)), - PostDeleteAt: model.NewPointer(int64(3)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@test.com"), - UserId: model.NewPointer("user-id"), - Username: model.NewPointer("username"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"post-id-3"}, - }, - } - - attachments := map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1", - Id: "test1", - Path: "test1", - }, - }, - "post-id-3": { - { - Name: "test2", - Id: "test2", - Path: "test2", - }, - }, - } - - tempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(tempDir) - assert.NoError(t, err) - }) - - config := filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: tempDir, - } - - fileBackend, err := filestore.NewFileBackend(config) - assert.NoError(t, err) - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - for postId := range attachments { - call := mockStore.FileInfoStore.On("GetForPost", postId, true, true, false).Times(2) - call.Run(func(args mock.Arguments) { - call.Return(attachments[args.Get(0).(string)], nil) - }) - } - - exportFileName := path.Join("export", "jobName", "jobName-batch001-csv.zip") - results, err := CsvExport(rctx, shared.ExportParams{ - ExportType: "", - ChannelMetadata: metadata, - Posts: posts, - ChannelMemberHistories: cmhs, - BatchPath: exportFileName, - BatchStartTime: 1, - BatchEndTime: 100, - Config: nil, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: fileBackend, - ExportBackend: fileBackend, - }) - assert.NoError(t, err) - assert.Equal(t, 2, results.NumWarnings) - - zipBytes, err := fileBackend.ReadFile(exportFileName) - assert.NoError(t, err) - - t.Cleanup(func() { - err = fileBackend.RemoveFile(exportFileName) - assert.NoError(t, err) - }) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - assert.NoError(t, err) - assert.Len(t, zipReader.File, 3) - warningsTxt, err := zipReader.Open("warning.txt") - assert.NoError(t, err) - data, err := io.ReadAll(warningsTxt) - assert.NoError(t, err) - warnings := string(data) - - expectedWarnings := fmt.Sprintf("Warning:%[1]s - Post: post-id-1 - test1\nWarning:%[1]s - Post: post-id-3 - test2\n", - shared.MissingFileMessageDuringBackendRead) - - assert.Equal(t, expectedWarnings, warnings) -} diff --git a/server/enterprise/message_export/global_relay_export/deliver.go b/server/enterprise/message_export/global_relay_export/deliver.go deleted file mode 100644 index 329dc0a040c..00000000000 --- a/server/enterprise/message_export/global_relay_export/deliver.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "archive/zip" - "context" - "fmt" - "io" - "net/mail" - "net/smtp" - "os" - "time" - - "github.com/mattermost/mattermost/server/public/model" -) - -func Deliver(export *os.File, config *model.Config) error { - info, err := export.Stat() - if err != nil { - return fmt.Errorf("unable to get the information of the export temporary file: %w", err) - } - zipFile, err := zip.NewReader(export, info.Size()) - if err != nil { - return fmt.Errorf("unable to open the export temporary file: %w", err) - } - - to := *config.MessageExportSettings.GlobalRelaySettings.EmailAddress - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Duration(*config.EmailSettings.SMTPServerTimeout)*time.Second) - defer cancel() - - conn, err := connectToSMTPServer(ctx, config) - if err != nil { - return fmt.Errorf("unable to connect to the smtp server: %w", err) - } - defer conn.Close() - - mailsCount := 0 - for _, mail := range zipFile.File { - from, err := getFrom(mail) - if err != nil { - return err - } - if err := deliverEmail(conn, mail, from, to); err != nil { - return err - } - - mailsCount++ - if mailsCount == MaxEmailsPerConnection { - mailsCount = 0 - conn.Close() - - var nErr error - conn, nErr = connectToSMTPServer(context.Background(), config) - if nErr != nil { - return fmt.Errorf("unable to connect to the smtp server: %w", nErr) - } - } - } - return nil -} - -func deliverEmail(c *smtp.Client, mailFile *zip.File, from string, to string) error { - mailData, err := mailFile.Open() - if err != nil { - return fmt.Errorf("unable to get the an email from the temporary file: %w", err) - } - defer mailData.Close() - - err = c.Mail(from) - if err != nil { - return fmt.Errorf("unable to set the email From address: %w", err) - } - - err = c.Rcpt(to) - if err != nil { - return fmt.Errorf("unable to set the email To address: %w", err) - } - - w, err := c.Data() - if err != nil { - return fmt.Errorf("unable to write the email message: %w", err) - } - - _, err = io.Copy(w, mailData) - if err != nil { - return fmt.Errorf("unable to set the email message: %w", err) - } - err = w.Close() - if err != nil { - return fmt.Errorf("unable to deliver the email to Global Relay: %w", err) - } - return nil -} - -func getFrom(mailFile *zip.File) (string, error) { - mailData, err := mailFile.Open() - if err != nil { - return "", fmt.Errorf("unable to get the an email from the temporary file: %w", err) - } - defer mailData.Close() - - message, err := mail.ReadMessage(mailData) - if err != nil { - return "", fmt.Errorf("unable to read the email information: %w", err) - } - return message.Header.Get("From"), nil -} diff --git a/server/enterprise/message_export/global_relay_export/deliver_test.go b/server/enterprise/message_export/global_relay_export/deliver_test.go deleted file mode 100644 index bf1d369fb8d..00000000000 --- a/server/enterprise/message_export/global_relay_export/deliver_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "archive/zip" - "fmt" - "io" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - gomail "gopkg.in/mail.v2" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/platform/shared/mail" -) - -func TestDeliver(t *testing.T) { - config := &model.Config{} - config.SetDefaults() - config.MessageExportSettings.GlobalRelaySettings.CustomerType = model.NewPointer("INBUCKET") - config.MessageExportSettings.GlobalRelaySettings.EmailAddress = model.NewPointer("test-globalrelay-mailbox@test") - - t.Run("Testing invalid zip file", func(t *testing.T) { - emptyFile, err := os.CreateTemp("", "export") - require.NoError(t, err) - defer emptyFile.Close() - defer os.Remove(emptyFile.Name()) - - err = Deliver(emptyFile, config) - assert.Error(t, err) - }) - - t.Run("Testing empty zip file", func(t *testing.T) { - emptyZipFile, err := os.CreateTemp("", "export") - require.NoError(t, err) - zipFile := zip.NewWriter(emptyZipFile) - err = zipFile.Close() - require.NoError(t, err) - defer emptyZipFile.Close() - defer os.Remove(emptyZipFile.Name()) - - err = mail.DeleteMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.NoError(t, err) - - err = Deliver(emptyZipFile, config) - assert.NoError(t, err) - - _, err = mail.GetMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.Error(t, err) - }) - - t.Run("Testing zip file with one email", func(t *testing.T) { - headers := map[string][]string{ - "From": {"test@test.com"}, - "To": {*config.MessageExportSettings.GlobalRelaySettings.EmailAddress}, - "Subject": {encodeRFC2047Word("test")}, - "Content-Transfer-Encoding": {"8bit"}, - "Auto-Submitted": {"auto-generated"}, - "Precedence": {"bulk"}, - GlobalRelayMsgTypeHeader: {"Mattermost"}, - GlobalRelayChannelNameHeader: {encodeRFC2047Word("test")}, - GlobalRelayChannelIDHeader: {encodeRFC2047Word("test")}, - GlobalRelayChannelTypeHeader: {encodeRFC2047Word("test")}, - } - - m := gomail.NewMessage(gomail.SetCharset("UTF-8")) - m.SetHeaders(headers) - m.SetBody("text/plain", "test") - - emptyZipFile, err := os.CreateTemp("", "export") - require.NoError(t, err) - zipFile := zip.NewWriter(emptyZipFile) - file, err := zipFile.Create("test") - require.NoError(t, err) - _, err = m.WriteTo(file) - require.NoError(t, err) - - err = zipFile.Close() - require.NoError(t, err) - defer emptyZipFile.Close() - defer os.Remove(emptyZipFile.Name()) - - err = mail.DeleteMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.NoError(t, err) - - err = Deliver(emptyZipFile, config) - assert.NoError(t, err) - - mailbox, err := mail.GetMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.NoError(t, err) - require.Len(t, mailbox, 1) - }) - - t.Run("Testing zip file with 50 emails", func(t *testing.T) { - headers := map[string][]string{ - "From": {"test@test.com"}, - "To": {*config.MessageExportSettings.GlobalRelaySettings.EmailAddress}, - "Subject": {encodeRFC2047Word("test")}, - "Content-Transfer-Encoding": {"8bit"}, - "Auto-Submitted": {"auto-generated"}, - "Precedence": {"bulk"}, - GlobalRelayMsgTypeHeader: {"Mattermost"}, - GlobalRelayChannelNameHeader: {encodeRFC2047Word("test")}, - GlobalRelayChannelIDHeader: {encodeRFC2047Word("test")}, - GlobalRelayChannelTypeHeader: {encodeRFC2047Word("test")}, - } - m := gomail.NewMessage(gomail.SetCharset("UTF-8")) - m.SetHeaders(headers) - m.SetBody("text/plain", "test") - - emptyZipFile, err := os.CreateTemp("", "export") - require.NoError(t, err) - zipFile := zip.NewWriter(emptyZipFile) - for x := range 50 { - var file io.Writer - file, err = zipFile.Create(fmt.Sprintf("test-%d", x)) - require.NoError(t, err) - _, err = m.WriteTo(file) - require.NoError(t, err) - } - err = zipFile.Close() - require.NoError(t, err) - defer emptyZipFile.Close() - defer os.Remove(emptyZipFile.Name()) - - err = mail.DeleteMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.NoError(t, err) - - err = Deliver(emptyZipFile, config) - assert.NoError(t, err) - - mailbox, err := mail.GetMailBox(*config.MessageExportSettings.GlobalRelaySettings.EmailAddress) - require.NoError(t, err) - require.Len(t, mailbox, 50) - }) -} diff --git a/server/enterprise/message_export/global_relay_export/global_relay_export.go b/server/enterprise/message_export/global_relay_export/global_relay_export.go deleted file mode 100644 index 070696245b8..00000000000 --- a/server/enterprise/message_export/global_relay_export/global_relay_export.go +++ /dev/null @@ -1,415 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "archive/zip" - "encoding/json" - "fmt" - "io" - "mime" - "os" - "sort" - "strings" - "time" - - "github.com/jaytaylor/html2text" - gomail "gopkg.in/mail.v2" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/enterprise/internal/file" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" - "github.com/mattermost/mattermost/server/v8/platform/shared/templates" -) - -const ( - GlobalRelayMsgTypeHeader = "X-GlobalRelay-MsgType" - GlobalRelayChannelNameHeader = "X-Mattermost-ChannelName" - GlobalRelayChannelIDHeader = "X-Mattermost-ChannelID" - GlobalRelayChannelTypeHeader = "X-Mattermost-ChannelType" - MaxEmailsPerConnection = 400 -) - -// MaxEmailBytes is a var because it needs to be set in tests. Otherwise it shouldn't be touched. -var MaxEmailBytes int64 = 250 * 1024 * 1024 // 250MB - -type ChannelExport struct { - TeamId string - TeamName string - TeamDisplayName string - ChannelId string // the unique id of the channel - ChannelName string // the name of the channel - ChannelDisplayName string - ChannelType model.ChannelType // the channel type - StartTime int64 // utc timestamp (seconds), start of export period or create time of channel, whichever is greater. Example: 1366611728. - EndTime int64 // utc timestamp (seconds), end of export period or delete time of channel, whichever is lesser. Example: 1366611728. - Participants []ParticipantRow // summary information about the conversation participants - Messages []Message // the messages that were sent during the conversation - ExportedOn int64 // utc timestamp (seconds), when this export was generated - numUserMessages map[string]int // key is user id, value is number of messages that they sent during this period - uploadedFiles []*model.FileInfo // any files that were uploaded to the channel during the export period - bytes int64 -} - -// a row in the summary table at the top of the export -type ParticipantRow struct { - shared.JoinExport - MessagesSent int -} - -type Message struct { - Id string - SentTime int64 - SenderId string - SenderUsername string - PostUsername string - SenderUserType string - SenderEmail string - Message string - PreviewsPost string - UpdateAt int64 - UpdateType shared.PostUpdatedType - EditedNewMsgId string -} - -func GlobalRelayExport(rctx request.CTX, p shared.ExportParams) (shared.RunExportResults, error) { - exportData, err := shared.GetGenericExportData(p) - results := exportData.Results - if err != nil { - return results, err - } - - var attachmentsRemovedPostIDs []string - allExports := make(map[string][]*ChannelExport, len(exportData.Exports)) - - // save a pointer to the joins for each channel, to be used later in getParticipants - joinsByChannel := make(map[string][]shared.JoinExport, len(exportData.Exports)) - - for _, channel := range exportData.Exports { - for _, post := range channel.Posts { - attachmentsRemoved := addToExports(rctx, allExports, channel, post) - attachmentsRemovedPostIDs = append(attachmentsRemovedPostIDs, attachmentsRemoved...) - } - if len(channel.Posts) == 0 { - // channel has no posts, but it was exported anyway, so it must have joins and leaves. - if _, present := allExports[channel.ChannelId]; !present { - // we found a new channel - allExports[channel.ChannelId] = []*ChannelExport{genericChannelToChannelExport(channel)} - } - } - joinsByChannel[channel.ChannelId] = channel.JoinEvents - } - - tmpFile, err := os.CreateTemp("", "") - if err != nil { - return results, fmt.Errorf("unable to open the temporary export file: %w", err) - } - defer file.DeleteTemp(rctx.Logger(), tmpFile) - zipFile := zip.NewWriter(tmpFile) - - // export each channelExport (sometimes multiple channelExport per real channel) - for _, channelExportList := range allExports { - for batchId, channelExport := range channelExportList { - // we need to make the participant list for each channelExport "batch" (multiple "batches"per real channel) - // because each batch will have its own number of messages for that participant. - channelExport.Participants = getParticipants(channelExport, joinsByChannel[channelExport.ChannelId]) - channelExport.ExportedOn = p.JobStartTime - - var channelExportFile io.Writer - channelExportFile, err = zipFile.Create(fmt.Sprintf("%s - (%s) - %d.eml", channelExport.ChannelName, channelExport.ChannelId, batchId)) - if err != nil { - return results, fmt.Errorf("unable to create the eml file: %w", err) - } - - if results.NumWarnings, err = generateEmail(rctx, p.FileAttachmentBackend, channelExport, p.Templates, channelExportFile); err != nil { - return results, err - } - } - } - - err = zipFile.Close() - if err != nil { - return results, fmt.Errorf("unable to close the zip file using tmpFile.Name: %v, err: %w", tmpFile.Name(), err) - } - - _, err = tmpFile.Seek(0, 0) - if err != nil { - return results, fmt.Errorf("unable to re-read the Global Relay temporary export file using tmpFile.Name: %v, err: %w", tmpFile.Name(), err) - } - - if p.ExportType == model.ComplianceExportTypeGlobalrelayZip { - // Try to disable the write timeout for the potentially big export file. - _, err = filestore.TryWriteFileContext(rctx.Context(), p.ExportBackend, tmpFile, p.BatchPath) - if err != nil { - return results, fmt.Errorf("unable to write the global relay file, using tmpFile.Name: %v, batchPath: %v, err: %w", tmpFile.Name(), p.BatchPath, err) - } - } else { - err = Deliver(tmpFile, p.Config) - if err != nil { - return results, fmt.Errorf("unable to deliver tmpFile.Name: %v, err: %w", tmpFile.Name(), err) - } - } - - if len(attachmentsRemovedPostIDs) > 0 { - rctx.Logger().Warn("Global Relay Attachments Removed because they were too large to send to Global Relay", - mlog.Int("number_of_attachments_removed", len(attachmentsRemovedPostIDs))) - rctx.Logger().Warn("List of posts which had attachments removed", - mlog.Array("post_ids", attachmentsRemovedPostIDs)) - } - - return results, nil -} - -// addToExports adds the post to the allExports collection. allExports keeps a map of channelId->[]*ChannelExport. -// If a channelId has an existing []*ChannelExport, it adds post to the last ChannelExport in that list. -// If the last ChannelExport is too big, it starts a new ChannelExport and appends it to the list (a new "batch"). -func addToExports(rctx request.CTX, allExports map[string][]*ChannelExport, genericChannel shared.ChannelExport, - post shared.PostExport) []string { - var channelExport *ChannelExport - var attachmentsRemovedPostIDs []string - if channelExports, present := allExports[*post.ChannelId]; !present { - // we found a new channel - channelExport = genericChannelToChannelExport(genericChannel) - allExports[*post.ChannelId] = []*ChannelExport{channelExport} - } else { - // we already know about this channel - channelExport = channelExports[len(channelExports)-1] - } - - msgBytes := int64(len(*post.PostMessage)) - - // Create a new ChannelExport if it would be too many bytes to add the post. - // NOTE: we are only exporting attachment starts. - attachmentStarts := make([]*model.FileInfo, 0, len(post.AttachmentCreates)) - for _, start := range post.AttachmentCreates { - attachmentStarts = append(attachmentStarts, start.FileInfo) - } - fileBytes := fileInfoListBytes(attachmentStarts) - postBytes := fileBytes + msgBytes - // NOTE: This is only a rough estimate -- we're not including the txt or html portion of the email... - attachmentsAloneTooLargeToSend := fileBytes > MaxEmailBytes // Attachments must be removed from export, they're too big to send. - if attachmentsAloneTooLargeToSend { - postBytes -= fileBytes - } - postTooLargeForChannelBatch := channelExport.bytes+postBytes > MaxEmailBytes - - if attachmentsAloneTooLargeToSend { - attachmentsRemovedPostIDs = append(attachmentsRemovedPostIDs, *post.PostId) - } - - // new "batch" - if postTooLargeForChannelBatch { - channelExport = genericChannelToChannelExport(genericChannel) - allExports[*post.ChannelId] = append(allExports[*post.ChannelId], channelExport) - } - - addPostToChannelExport(rctx, channelExport, post) - - // if this post includes files, add them to the collection - addAttachmentsToChannelExport(channelExport, post, post.AttachmentCreates, post.AttachmentDeletes, attachmentsAloneTooLargeToSend) - channelExport.bytes += postBytes - return attachmentsRemovedPostIDs -} - -func genericChannelToChannelExport(genericChannel shared.ChannelExport) *ChannelExport { - return &ChannelExport{ - TeamId: genericChannel.TeamId, - TeamName: genericChannel.TeamName, - TeamDisplayName: genericChannel.TeamDisplayName, - ChannelId: genericChannel.ChannelId, - ChannelName: genericChannel.ChannelName, - ChannelDisplayName: genericChannel.DisplayName, - ChannelType: genericChannel.ChannelType, - StartTime: genericChannel.StartTime, - EndTime: genericChannel.EndTime, - // we can't preallocate sizes here because we don't know how many will be in this "batch" - Participants: make([]ParticipantRow, 0), - Messages: make([]Message, 0), - ExportedOn: 0, - numUserMessages: make(map[string]int), - uploadedFiles: make([]*model.FileInfo, 0), - bytes: 0, - } -} - -func getParticipants(channelExport *ChannelExport, joinEvents []shared.JoinExport) []ParticipantRow { - participants := make([]ParticipantRow, 0, len(joinEvents)) - for _, j := range joinEvents { - participants = append(participants, ParticipantRow{ - JoinExport: j, - MessagesSent: channelExport.numUserMessages[j.UserId], - }) - } - - sort.Slice(participants, func(i, j int) bool { - return participants[i].Username < participants[j].Username - }) - return participants -} - -func generateEmail(rctx request.CTX, fileAttachmentBackend filestore.FileBackend, channelExport *ChannelExport, templates *templates.Container, w io.Writer) (int, error) { - var warningCount int - participantEmailAddresses := getParticipantEmails(channelExport) - - // GlobalRelay expects the email to come from the person that initiated the conversation. - // our conversations aren't really initiated, so we just use the first person we find - from := participantEmailAddresses[0] - - // it also expects the email to be addressed to the other participants in the conversation - mimeTo := strings.Join(participantEmailAddresses, ",") - - htmlBody, err := channelExportToHTML(rctx, channelExport, templates) - if err != nil { - return warningCount, fmt.Errorf("unable to generate eml file data: %w", err) - } - - subject := fmt.Sprintf("Mattermost Compliance Export: %s", channelExport.ChannelDisplayName) - htmlMessage := "\r\n" + htmlBody + "" - - txtBody, err := html2text.FromString(htmlBody) - if err != nil { - rctx.Logger().Warn("Error transforming html to plain text for GlobalRelay email", mlog.Err(err)) - txtBody = "" - } - - headers := map[string][]string{ - "From": {from}, - "To": {mimeTo}, - "Subject": {encodeRFC2047Word(subject)}, - "Content-Transfer-Encoding": {"8bit"}, - "Auto-Submitted": {"auto-generated"}, - "Precedence": {"bulk"}, - GlobalRelayMsgTypeHeader: {"Mattermost"}, - GlobalRelayChannelNameHeader: {encodeRFC2047Word(channelExport.ChannelDisplayName)}, - GlobalRelayChannelIDHeader: {encodeRFC2047Word(channelExport.ChannelId)}, - GlobalRelayChannelTypeHeader: {encodeRFC2047Word(shared.ChannelTypeDisplayName(channelExport.ChannelType))}, - } - - m := gomail.NewMessage(gomail.SetCharset("UTF-8")) - m.SetHeaders(headers) - m.SetDateHeader("Date", time.Unix(channelExport.EndTime/1000, 0).UTC()) - m.SetBody("text/plain", txtBody) - m.AddAlternative("text/html", htmlMessage) - - for _, fileInfo := range channelExport.uploadedFiles { - path := fileInfo.Path - - m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error { - var reader filestore.ReadCloseSeeker - reader, err = fileAttachmentBackend.Reader(path) - if err != nil { - rctx.Logger().Warn("File not found for export", mlog.String("filename", path)) - warningCount += 1 - return nil - } - defer reader.Close() - - _, err = io.Copy(writer, reader) - if err != nil { - return fmt.Errorf("unable to add attachment to the Global Relay export: %w", err) - } - return nil - })) - } - - _, err = m.WriteTo(w) - if err != nil { - return warningCount, fmt.Errorf("unable to generate eml file data: %w", err) - } - return warningCount, nil -} - -func getParticipantEmails(channelExport *ChannelExport) []string { - participantEmails := make([]string, 0, len(channelExport.Participants)) - for _, participant := range channelExport.Participants { - participantEmails = append(participantEmails, participant.UserEmail) - } - return participantEmails -} - -func fileInfoListBytes(fileInfoList []*model.FileInfo) int64 { - totalBytes := int64(0) - for _, fileInfo := range fileInfoList { - totalBytes += fileInfo.Size - } - return totalBytes -} - -func addPostToChannelExport(rctx request.CTX, channelExport *ChannelExport, post shared.PostExport) { - strPostProps := post.PostProps - bytePostProps := []byte(*strPostProps) - - // Added to show the username if overridden by a webhook or API integration - originalUsername := model.SafeDereference(post.Username) - postUserName := originalUsername - var postPropsLocal map[string]any - err := json.Unmarshal(bytePostProps, &postPropsLocal) - if err != nil { - rctx.Logger().Warn("Failed to unmarshal post Props into JSON. Ignoring username override.", mlog.Err(err)) - } else { - if overrideUsername, ok := postPropsLocal[model.PostPropsOverrideUsername]; ok { - postUserName = overrideUsername.(string) - } - - if postUserName == originalUsername { - if overrideUsername, ok := postPropsLocal[model.PostPropsWebhookDisplayName]; ok { - postUserName = overrideUsername.(string) - } - } - } - - element := postToMessage(post) - element.PostUsername = postUserName - channelExport.Messages = append(channelExport.Messages, element) - channelExport.numUserMessages[*post.UserId] += 1 -} - -func postToMessage(post shared.PostExport) Message { - return Message{ - Id: model.SafeDereference(post.PostId), - SentTime: model.SafeDereference(post.PostCreateAt), - SenderId: model.SafeDereference(post.UserId), - SenderUsername: model.SafeDereference(post.Username), - PostUsername: model.SafeDereference(post.Username), - SenderUserType: string(post.UserType), - SenderEmail: model.SafeDereference(post.UserEmail), - Message: post.Message, - PreviewsPost: post.PreviewID(), - UpdateAt: post.UpdateAt, - UpdateType: post.UpdatedType, - EditedNewMsgId: post.EditedNewMsgId, - } -} - -func addAttachmentsToChannelExport(channelExport *ChannelExport, post shared.PostExport, - attachmentStarts []*shared.FileUploadStartExport, attachmentDeletes []shared.PostExport, removeAttachments bool) { - for _, start := range attachmentStarts { - var message string - - if removeAttachments { - message = fmt.Sprintf("Uploaded file %q (id '%s') was removed because it was too large to send.", - start.FileInfo.Name, start.FileInfo.Id) - } else { - channelExport.uploadedFiles = append(channelExport.uploadedFiles, start.FileInfo) - message = fmt.Sprintf("Uploaded file %s", start.FileInfo.Name) - } - - uploadElement := postToMessage(post) - uploadElement.Message = message - channelExport.Messages = append(channelExport.Messages, uploadElement) - } - - for _, deleted := range attachmentDeletes { - uploadElement := postToMessage(deleted) - uploadElement.Message = fmt.Sprintf("Deleted file %s", deleted.FileInfo.Name) - channelExport.Messages = append(channelExport.Messages, uploadElement) - } -} - -func encodeRFC2047Word(s string) string { - return mime.BEncoding.Encode("utf-8", s) -} diff --git a/server/enterprise/message_export/global_relay_export/global_relay_export_test.go b/server/enterprise/message_export/global_relay_export/global_relay_export_test.go deleted file mode 100644 index a6975832362..00000000000 --- a/server/enterprise/message_export/global_relay_export/global_relay_export_test.go +++ /dev/null @@ -1,3126 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "archive/zip" - "bytes" - "encoding/base64" - "fmt" - "io" - "os" - "path" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" - "github.com/mattermost/mattermost/server/v8/platform/shared/templates" -) - -func TestGlobalRelayExport(t *testing.T) { - templatesDir, ok := fileutils.FindDir("templates") - require.True(t, ok) - - templatesContainer, err := templates.New(templatesDir) - require.NotNil(t, templatesContainer) - require.NoError(t, err) - - tempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(tempDir) - assert.NoError(t, err) - }) - - rctx := request.TestContext(t) - - config := filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: tempDir, - } - - fileBackend, err := filestore.NewFileBackend(config) - assert.NoError(t, err) - exportBackend := fileBackend - - chanTypeDirect := model.ChannelTypeDirect - grExportTests := []struct { - name string - cmhs map[string][]*model.ChannelMemberHistoryResult - metadata map[string]*shared.MetadataChannel - startTime int64 - endTime int64 - posts []*model.MessageExport - attachments map[string][]*model.FileInfo - attachmentsContent map[string]string - expectedAttachmentContent [][]string - maxEmailBytes int64 - numExpectedEmls int - expectedHeaders [][]string - expectedTexts [][]string - notExpectedTexts [][]string - expectedHTMLs [][]string - expectedWarnings int - empty bool - }{ - { - name: "empty", - cmhs: map[string][]*model.ChannelMemberHistoryResult{}, - posts: []*model.MessageExport{}, - attachments: map[string][]*model.FileInfo{}, - expectedWarnings: 0, - empty: true, - }, - { - name: "posts", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "id-test1", UserEmail: "test1@test.com", Username: "test1", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "id-test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "id-test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message 1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("id-test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id2"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message 2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("id-test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id1 1970-01-01T00:00:00Z @test1 id-test1 @test1 user (test1@test.com=", - ") message 1", - "* post-id2 1970-01-01T00:01:40Z @test1 id-test1 @test1 user (test1@test.com=", - ") message 2", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " id-test1", - " @test1", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " id-test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " id-test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id1", - " 1970-01-01T00:00:00Z", - " @test1", - " id-test1", - " @test1", - " user", - " (test1@test.com)", - " message 1", - "
  • ", - "", - "
  • ", - " post-id2", - " 1970-01-01T00:01:40Z", - " @test1", - " id-test1", - " @test1", - " user", - " (test1@test.com)", - " message 2", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - - { - name: "posts with attachments, size ok", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test2"}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - }, - }, - "post-id-2": { - { - Name: "test2-attachment", - Id: "test2-attachment", - Path: "test2-attachment", - CreateAt: 1, - }, - }, - }, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - "test2-attachment": "this is the second attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - {base64.StdEncoding.EncodeToString([]byte("this is the second attachment content"))}, - }, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message1", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message2", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message1", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message2", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - - { - name: "posts with attachments, size too large, new channel export needed", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test2"}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - Size: 90, // 90 + 8 for message = message of 98 bytes. next one will be too big. - }, - }, - "post-id-2": { - { - Name: "test2-attachment", - Id: "test2-attachment", - Path: "test2-attachment", - Size: 56, - }, - }, - }, - maxEmailBytes: 100, - numExpectedEmls: 2, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - "test2-attachment": "this is the second attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - {base64.StdEncoding.EncodeToString([]byte("this is the second attachment content"))}, - }, - expectedHeaders: [][]string{ - // eml 0 - { - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }, - // eml 1 - { - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }, - }, - - expectedTexts: [][]string{ - // eml 0 - { - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message1", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }, - // eml 1 - { - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message2", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test2-attachment", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test2-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test2-attachment\"", - }, "\r\n"), - }, - }, - - expectedHTMLs: [][]string{ - // eml 0 - { - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 1", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message1", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - }, "\r\n"), - }, - // eml 1 - { - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 1", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - strings.Join([]string{ - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message2", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test2-attachment", - "
  • ", - }, "\r\n"), - }, - }, - expectedWarnings: 0, - }, - - { - name: "posts with attachments, attachment too large, remove it but only one eml needed", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - Size: 101, - }, - }, - }, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - }, - expectedAttachmentContent: [][]string{ - {""}, - }, - maxEmailBytes: 100, - numExpectedEmls: 1, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message1", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file \"test1-attachment\" (id 'test1-attachment') was removed becaus=", - "e it was too large to send.", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message2", - }, "\r\n"), - }}, - - notExpectedTexts: [][]string{{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - }}, - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message1", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file "test1-attachment" (id &#=", - "39;test1-attachment') was removed because it was too large to send.", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message2", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - }, - - { - name: "posts with attachments, post size and attachment too large, new channel export needed and attachment removed", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test2"}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - Size: 20, // 20 + 8 for message = message of 28 bytes. next one will be too big. - }, - }, - "post-id-2": { - { - Name: "test2-attachment", - Id: "test2-attachment", - Path: "test2-attachment", - Size: 31, // too big, will be deleted - }, - }, - }, - maxEmailBytes: 30, - numExpectedEmls: 2, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - "test2-attachment": "this is the second attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - {""}, - }, - notExpectedTexts: [][]string{ - // eml 0 - { - "Content-Disposition: attachment; filename=\"test2-attachment\"", - }, - // eml 1 - { - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Disposition: attachment; filename=\"test2-attachment\"", - }, - }, - - expectedHeaders: [][]string{ - // eml 0 - { - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }, - // eml 1 - { - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }, - }, - - expectedTexts: [][]string{ - // eml 0 - { - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message1", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }, - // eml 1 - { - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message2", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file \"test2-attachment\" (id 'test2-attachment') was removed becaus=", - "e it was too large to send.", - }, "\r\n"), - }, - }, - - expectedHTMLs: [][]string{ - // eml 0 - { - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 1", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message1", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - }, "\r\n"), - }, - // eml 1 - { - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 1", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - strings.Join([]string{ - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message2", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file "test2-attachment" (id &#=", - "39;test2-attachment') was removed because it was too large to send.", - "
  • ", - }, "\r\n"), - }, - }, - expectedWarnings: 0, - }, - - { - name: "posts with multiple attachments, size ok", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message1"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1", "test1-2"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message2"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - }, - { - Name: "test1-attachment-2", - Id: "test1-attachment-2", - Path: "test1-attachment-2", - CreateAt: 1, - }, - }, - }, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - "test1-attachment-2": "this is the second attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - {base64.StdEncoding.EncodeToString([]byte("this is the second attachment content"))}, - }, - - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message1", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment-2", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message2", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment-2\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment-2\"", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message1", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment-2", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message2", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - - { - name: "posts with deleted post and deleted attachments", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 200_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(100_000)), - PostUpdateAt: model.NewPointer(int64(200_000)), - PostDeleteAt: model.NewPointer(int64(200_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{\"deleteBy\":\"fy8j97gwii84bk4zxprbpc9d9w\"}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - UpdateAt: 200_000, - DeleteAt: 200_000, - }, - }, - }, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - }, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:03:20 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:03:20Z", - "* Duration: 3 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Deleted file test1-attachment FileDeleted 1970-01-01T00:03:20Z", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "delete message Deleted 1970-01-01T00:03:20Z", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 3", - "", - "", "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:03:20Z", - " 3 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - "", "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Deleted file test1-attachment", - " FileDeleted", - " 1970-01-01T00:03:20Z", - " ", - "
  • ", - "", "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " delete message", - " Deleted", - " 1970-01-01T00:03:20Z", - " ", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - - { - name: "posts with deleted attachments, no deleted post, and at different time from original post", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - UpdateAt: 200_000, - DeleteAt: 200_000, - }, - }, - }, - attachmentsContent: map[string]string{ - "test1-attachment": "this is the attachment content", - }, - expectedAttachmentContent: [][]string{ - {base64.StdEncoding.EncodeToString([]byte("this is the attachment content"))}, - }, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Deleted file test1-attachment FileDeleted 1970-01-01T00:03:20Z", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Deleted file test1-attachment", - " FileDeleted", - " 1970-01-01T00:03:20Z", - " ", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - - { - name: "posts with missing attachments", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id-1"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{"test1"}, - }, - { - PostId: model.NewPointer("post-id-2"), - PostOriginalId: model.NewPointer("post-original-id"), - PostRootId: model.NewPointer("post-id-1"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{ - "post-id-1": { - { - Name: "test1-attachment", - Id: "test1-attachment", - Path: "test1-attachment", - CreateAt: 1, - }, - }, - }, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "message", - "* post-id-1 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) =", - "Uploaded file test1-attachment", - "* post-id-2 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) =", - "message", - }, "\r\n"), - strings.Join([]string{ - "Content-Disposition: attachment; filename=\"test1-attachment\"", - "Content-Transfer-Encoding: base64", - "Content-Type: application/octet-stream; name=\"test1-attachment\"", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", - "
  • ", - " post-id-1", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " Uploaded file test1-attachment", - "
  • ", - "", - "
  • ", - " post-id-2", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 1, - numExpectedEmls: 1, - }, - - { - name: "posts with override_username property", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{\"from_webhook\":\"true\",\"html\":\"Test HTML\",\"override_username\":\"test_username_override\",\"webhook_display_name\":\"Test Bot\"}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) me=", - "ssage", - "* post-id 1970-01-01T00:01:40Z @test1 test1 @test_username_override user (t=", - "est1@test.com) message", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", - "
  • ", - " post-id", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test_username_override", - " user", - " (test1@test.com)", - " message", - "
  • ", - }, "\r\n"), - }}, - numExpectedEmls: 1, - }, - - { - name: "posts with webhook_display_name property", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{\"from_webhook\":\"true\",\"webhook_display_name\":\"Test Bot\"}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) me=", - "ssage", - "* post-id 1970-01-01T00:01:40Z @test1 test1 @Test Bot user (test1@test.com)=", - " message", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - "", - "
  • ", - " post-id", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @Test Bot", - " user", - " (test1@test.com)", - " message", - "
  • ", - }, "\r\n"), - }}, - numExpectedEmls: 1, - }, - - { - name: "post with permalink preview", - cmhs: map[string][]*model.ChannelMemberHistoryResult{ - "channel-id": { - { - JoinTime: 0, UserId: "test1", UserEmail: "test1@test.com", Username: "test", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 8, UserId: "test2", UserEmail: "test2@test.com", Username: "test2", LeaveTime: model.NewPointer(int64(100_000)), - }, - { - JoinTime: 400, UserId: "test3", UserEmail: "test3@test.com", Username: "test3", - }, - }, - }, - metadata: map[string]*shared.MetadataChannel{ - "channel-id": { - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: "channel-id", - ChannelName: "channel-name", - ChannelDisplayName: "channel-display-name", - ChannelType: chanTypeDirect, - RoomId: "direct - channel-id", - StartTime: 1, - EndTime: 100, - }, - }, - startTime: 1, - endTime: 100_000, - posts: []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(1)), - PostCreateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer(`{"previewed_post":"o4w39mc1ff8y5fite4b8hacy1x"}`), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - PostRootId: model.NewPointer("post-root-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostUpdateAt: model.NewPointer(int64(100_000)), - PostCreateAt: model.NewPointer(int64(100_000)), - PostMessage: model.NewPointer("message"), - PostProps: model.NewPointer("{}"), - PostType: model.NewPointer(""), - UserEmail: model.NewPointer("test1@test.com"), - UserId: model.NewPointer("test1"), - Username: model.NewPointer("test1"), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - }, - attachments: map[string][]*model.FileInfo{}, - expectedHeaders: [][]string{{ - "MIME-Version: 1.0", - "X-Mattermost-ChannelType: direct", - "Content-Transfer-Encoding: 8bit", - "Precedence: bulk", - "X-GlobalRelay-MsgType: Mattermost", - "X-Mattermost-ChannelID: channel-id", - "X-Mattermost-ChannelName: channel-display-name", - "Auto-Submitted: auto-generated", - "Date: Thu, 01 Jan 1970 00:01:40 +0000", - "From: test1@test.com", - "To: test1@test.com,test2@test.com", - "Subject: Mattermost Compliance Export: channel-display-name", - }}, - - expectedTexts: [][]string{{ - strings.Join([]string{ - "* TeamId: team-id", - "* TeamName: team-name", - "* TeamDisplayName: team-display-name", - "* ChannelId: channel-id", - "* ChannelName: channel-name", - "* ChannelDisplayName: channel-display-name", - "* Started: 1970-01-01T00:00:00Z", - "* Ended: 1970-01-01T00:01:40Z", - "* Duration: 2 minutes", - }, "\r\n"), - strings.Join([]string{ - "--------", - "Messages", - "--------", - "", - "* post-id 1970-01-01T00:00:00Z @test1 test1 @test1 user (test1@test.com) me=", - "ssage o4w39mc1ff8y5fite4b8hacy1x", - "* post-id 1970-01-01T00:01:40Z @test1 test1 @test1 user (test1@test.com) me=", - "ssage", - }, "\r\n"), - }}, - - expectedHTMLs: [][]string{{ - strings.Join([]string{ - " ", - }, "\r\n"), - strings.Join([]string{ - "", - " test1", - " @test", - " user", - " test1@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 2", - "", - "", - "", - " test2", - " @test2", - " user", - " test2@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - "", - "", - " test3", - " @test3", - " user", - " test3@test.com", - " 1970-01-01T00:00:00Z", - " 1970-01-01T00:01:40Z", - " 2 minutes", - " 0", - "", - }, "\r\n"), - - strings.Join([]string{ - "
  • ", - " post-id", - " 1970-01-01T00:00:00Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - " o4w39mc1ff8y5fite4b8hacy1x", - "
  • ", - "", - "
  • ", - " post-id", - " 1970-01-01T00:01:40Z", - " @test1", - " test1", - " @test1", - " user", - " (test1@test.com)", - " message", - "
  • ", - }, "\r\n"), - }}, - expectedWarnings: 0, - numExpectedEmls: 1, - }, - } - - for _, tt := range grExportTests { - t.Run(tt.name, func(t *testing.T) { - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - if len(tt.attachments) > 0 { - for postId, attachments := range tt.attachments { - call := mockStore.FileInfoStore.On("GetForPost", postId, true, true, false) - call.Run(func(args mock.Arguments) { - call.Return(attachments, nil) - }) - for _, attachment := range attachments { - if tt.expectedWarnings == 0 { - content, ok := tt.attachmentsContent[attachment.Id] - require.True(t, ok, "attachment not found for id: %s", attachment.Id) - _, err = fileBackend.WriteFile(strings.NewReader(content), attachment.Path) - require.NoError(t, err) - - t.Cleanup(func() { - err = fileBackend.RemoveFile(attachment.Path) - assert.NoError(t, err) - }) - } - } - } - } - - if tt.maxEmailBytes > 0 { - origMaxEmailBytes := MaxEmailBytes - MaxEmailBytes = tt.maxEmailBytes - t.Cleanup(func() { - MaxEmailBytes = origMaxEmailBytes - }) - } - - exportFileName := path.Join("export", "jobName", "jobName-batch001.zip") - results, err := GlobalRelayExport(rctx, shared.ExportParams{ - ExportType: model.ComplianceExportTypeGlobalrelayZip, - ChannelMetadata: tt.metadata, - Posts: tt.posts, - ChannelMemberHistories: tt.cmhs, - BatchPath: exportFileName, - BatchStartTime: tt.startTime, - BatchEndTime: tt.endTime, - Db: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: fileBackend, - ExportBackend: exportBackend, - Templates: templatesContainer, - }) - assert.NoError(t, err) - assert.Equal(t, tt.expectedWarnings, results.NumWarnings) - - // channel-name - (channel-id) - 0.eml - if !tt.empty { - if tt.numExpectedEmls == 0 { - require.True(t, false, "need numExpectedEmls to be at least 1") - } - openZipAndAssertNumEmls(t, exportBackend, exportFileName, tt.numExpectedEmls) - - for batchNum := 0; batchNum < tt.numExpectedEmls; batchNum++ { - metadata := tt.metadata["channel-id"] - emlName := fmt.Sprintf("%s - (%s) - %d.eml", metadata.ChannelName, - metadata.ChannelId, batchNum) - eml := openZipAndReadFileStartingWith(t, exportBackend, exportFileName, emlName) - - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><> batchNum %d actual: \n%s", batchNum, eml) - - t.Run("headers", func(t *testing.T) { - for _, expectedHeader := range tt.expectedHeaders[batchNum] { - assert.Contains(t, eml, expectedHeader, "batchNum %d, expected: %s", batchNum, expectedHeader) - } - }) - - t.Run("text-version", func(t *testing.T) { - for _, expectedText := range tt.expectedTexts[batchNum] { - assert.Contains(t, eml, expectedText, "batchNum %d, expected: %s", batchNum, expectedText) - } - if len(tt.notExpectedTexts) > 0 { - for _, notExpectedText := range tt.notExpectedTexts[batchNum] { - assert.NotContains(t, eml, notExpectedText, "batchNum %d, expected: %s", batchNum, notExpectedText) - } - } - }) - - t.Run("html-version", func(t *testing.T) { - for _, expectedHTML := range tt.expectedHTMLs[batchNum] { - assert.Contains(t, eml, expectedHTML, "batchNum %d, \nexpected:\n %s\n\nactual:\n %s\n", batchNum, expectedHTML, eml) - } - }) - - assert.Len(t, tt.expectedAttachmentContent, len(tt.attachmentsContent), "every attachmentsContent should have an expectedAttachmentContent") - - if len(tt.expectedAttachmentContent) > 0 { - t.Run("file encoding", func(t *testing.T) { - for _, expected := range tt.expectedAttachmentContent[batchNum] { - assert.Contains(t, eml, expected, "batchNum %d, \nexpected:\n %s\n\nactual:\n %s\n", batchNum, expected, eml) - } - }) - } - } - } - }) - } -} - -func openZipAndReadFileStartingWith(t *testing.T, backend filestore.FileBackend, path string, startsWith string) string { - zipBytes, err := backend.ReadFile(path) - require.NoError(t, err) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - var names []string - for _, f := range zipReader.File { - if strings.HasPrefix(f.Name, startsWith) { - file, err := f.Open() - require.NoError(t, err) - contents, err := io.ReadAll(file) - require.NoError(t, err) - err = file.Close() - require.NoError(t, err) - - return string(contents) - } - names = append(names, f.Name) - } - - require.True(t, false, "called openZipAndReadFileStartingWith but didn't file file starting with: %s. Found: %v", startsWith, names) - return "" -} - -func openZipAndAssertNumEmls(t *testing.T, backend filestore.FileBackend, path string, numEmls int) { - zipBytes, err := backend.ReadFile(path) - require.NoError(t, err) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - require.Len(t, zipReader.File, numEmls, "numEmls wrong") -} diff --git a/server/enterprise/message_export/global_relay_export/main_test.go b/server/enterprise/message_export/global_relay_export/main_test.go deleted file mode 100644 index 3a1822aa6e9..00000000000 --- a/server/enterprise/message_export/global_relay_export/main_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/channels/testlib" -) - -var mainHelper *testlib.MainHelper - -func TestMain(m *testing.M) { - var options = testlib.HelperOptions{ - EnableResources: true, - } - - mainHelper = testlib.NewMainHelperWithOptions(&options) - defer mainHelper.Close() - - mainHelper.Main(m) -} diff --git a/server/enterprise/message_export/global_relay_export/smtp.go b/server/enterprise/message_export/global_relay_export/smtp.go deleted file mode 100644 index adbd5a3ab8d..00000000000 --- a/server/enterprise/message_export/global_relay_export/smtp.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "context" - "net/smtp" - "os" - "strconv" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/utils" - "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" - "github.com/mattermost/mattermost/server/v8/platform/shared/mail" -) - -const ( - GlobalRelayA9Server = "mailarchivespool1.globalrelay.com" - GlobalRelayA10Server = "feeds.globalrelay.com" - GlobalRelayA9IP = "208.81.212.70" - GlobalRelayA10IP = "208.81.213.24" - - defaultSMTPPort = "25" - defaultInbucketSMTPPort = "10025" -) - -func connectToSMTPServer(ctx context.Context, config *model.Config) (*smtp.Client, error) { - smtpServerName := "" - smtpServerHost := "" - smtpPort := defaultSMTPPort - security := model.ConnSecurityStarttls - auth := true - if *config.MessageExportSettings.GlobalRelaySettings.CustomerType == "A10" { - smtpServerName = GlobalRelayA10Server - smtpServerHost = GlobalRelayA10IP - } else if *config.MessageExportSettings.GlobalRelaySettings.CustomerType == "A9" { - smtpServerName = GlobalRelayA9Server - smtpServerHost = GlobalRelayA9IP - } else if *config.MessageExportSettings.GlobalRelaySettings.CustomerType == "INBUCKET" { - inbucketSMTPPort := os.Getenv("CI_INBUCKET_SMTP_PORT") - if inbucketSMTPPort == "" { - inbucketSMTPPort = defaultInbucketSMTPPort - } - inbucketHost := os.Getenv("CI_INBUCKET_HOST") - if inbucketHost == "" { - intPort, err := strconv.Atoi(inbucketSMTPPort) - if err != nil { - intPort = 0 - } - inbucketHost = testutils.GetInterface(intPort) - } - smtpServerName = inbucketHost - smtpServerHost = inbucketHost - smtpPort = inbucketSMTPPort - auth = false - } else if *config.MessageExportSettings.GlobalRelaySettings.CustomerType == model.GlobalrelayCustomerTypeCustom { - customSMTPPort := *config.MessageExportSettings.GlobalRelaySettings.CustomSMTPPort - if customSMTPPort != "" { - smtpPort = customSMTPPort - } - smtpServerName = *config.MessageExportSettings.GlobalRelaySettings.CustomSMTPServerName - smtpServerHost = *config.MessageExportSettings.GlobalRelaySettings.CustomSMTPServerName - } - - smtpConfig := &mail.SMTPConfig{ - ConnectionSecurity: security, - SkipServerCertificateVerification: false, - Hostname: utils.GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL), - ServerName: smtpServerName, - Server: smtpServerHost, - Port: smtpPort, - EnableSMTPAuth: auth, - Username: *config.MessageExportSettings.GlobalRelaySettings.SMTPUsername, - Password: *config.MessageExportSettings.GlobalRelaySettings.SMTPPassword, - ServerTimeout: *config.MessageExportSettings.GlobalRelaySettings.SMTPServerTimeout, - } - conn, err1 := mail.ConnectToSMTPServerAdvanced(smtpConfig) - if err1 != nil { - return nil, err1 - } - - c, err2 := mail.NewSMTPClientAdvanced( - ctx, - conn, - smtpConfig, - ) - if err2 != nil { - conn.Close() - return nil, err2 - } - return c, nil -} diff --git a/server/enterprise/message_export/global_relay_export/test_helpers.go b/server/enterprise/message_export/global_relay_export/test_helpers.go deleted file mode 100644 index 28bc8aaf973..00000000000 --- a/server/enterprise/message_export/global_relay_export/test_helpers.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "net/mail" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func AssertHeaderContains(t *testing.T, msg string, expected map[string]string) { - t.Helper() - m, err := mail.ReadMessage(strings.NewReader(msg)) - require.NoError(t, err) - - for k, v := range expected { - assert.Equal(t, v, m.Header.Get(k)) - } -} - -func CleanTestOutput(msg string) string { - msg = strings.Replace(msg, "=\r\n", "", -1) - msg = strings.Replace(msg, "\r\n", "\n", -1) - return msg -} diff --git a/server/enterprise/message_export/global_relay_export/to_html.go b/server/enterprise/message_export/global_relay_export/to_html.go deleted file mode 100644 index 4b59992fe52..00000000000 --- a/server/enterprise/message_export/global_relay_export/to_html.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package global_relay_export - -import ( - "bytes" - "html/template" - "sort" - "strings" - "time" - - "github.com/hako/durafmt" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/platform/shared/templates" -) - -func TimestampConvert(timestampMS int64) string { - return time.Unix(timestampMS/1000, 0).UTC().Format(time.RFC3339) - - // for testing: (keep in case we need to be specific -- helps when you have joins and leaves within millis of each other) - //return fmt.Sprintf("%d", timestampMS) -} - -func channelExportToHTML(rctx request.CTX, channelExport *ChannelExport, t *templates.Container) (string, error) { - durationMilliseconds := channelExport.EndTime - channelExport.StartTime - duration := time.Duration(durationMilliseconds) * time.Millisecond - - var participantRowsBuffer bytes.Buffer - for i := range channelExport.Participants { - participantHTML, err := participantToHTML(&channelExport.Participants[i], t) - if err != nil { - rctx.Logger().Error("Unable to render participant html for compliance export", mlog.Err(err)) - continue - } - participantRowsBuffer.WriteString(participantHTML) - } - - var messagesBuffer bytes.Buffer - sort.Slice(channelExport.Messages, func(i, j int) bool { - if channelExport.Messages[i].SentTime == channelExport.Messages[j].SentTime { - return !strings.HasPrefix(channelExport.Messages[i].Message, "Uploaded file") && - !strings.HasPrefix(channelExport.Messages[i].Message, "Deleted file") && - channelExport.Messages[i].UpdateType == "" - } - return channelExport.Messages[i].SentTime < channelExport.Messages[j].SentTime - }) - for i := range channelExport.Messages { - messageHTML, err := messageToHTML(&channelExport.Messages[i], t) - if err != nil { - rctx.Logger().Error("Unable to render message html for compliance export", mlog.Err(err)) - continue - } - messagesBuffer.WriteString(messageHTML) - } - - data := templates.Data{ - Props: map[string]any{ - "TeamId": channelExport.TeamId, - "TeamName": channelExport.TeamName, - "TeamDisplayName": channelExport.TeamDisplayName, - "ChannelId": channelExport.ChannelId, - "ChannelName": channelExport.ChannelName, - "ChannelDisplayName": channelExport.ChannelDisplayName, - "Started": TimestampConvert(channelExport.StartTime), - "Ended": TimestampConvert(channelExport.EndTime), - "Duration": durafmt.Parse(duration.Round(time.Minute)).String(), - "ParticipantRows": template.HTML(participantRowsBuffer.String()), - "Messages": template.HTML(messagesBuffer.String()), - "ExportDate": TimestampConvert(channelExport.ExportedOn), - }, - } - - return t.RenderToString("globalrelay_compliance_export", data) -} - -func participantToHTML(participant *ParticipantRow, t *templates.Container) (string, error) { - durationMilliseconds := participant.LeaveTime - participant.JoinTime - duration := time.Duration(durationMilliseconds) * time.Millisecond - - data := templates.Data{ - Props: map[string]any{ - "UserId": participant.UserId, - "Username": participant.Username, - "UserType": participant.UserType, - "Email": participant.UserEmail, - "Joined": TimestampConvert(participant.JoinTime), - "Left": TimestampConvert(participant.LeaveTime), - "Duration": durafmt.Parse(duration.Round(time.Minute)).String(), - "NumMessages": participant.MessagesSent, - }, - } - return t.RenderToString("globalrelay_compliance_export_participant_row", data) -} - -func messageToHTML(message *Message, t *templates.Container) (string, error) { - postUsername := message.PostUsername - // Added to improve readability - if postUsername != "" { - postUsername = "@" + postUsername - } - data := templates.Data{ - Props: map[string]any{ - "PostId": message.Id, - "SentTime": TimestampConvert(message.SentTime), - "UserId": message.SenderId, - "Username": message.SenderUsername, - "PostUsername": postUsername, - "UserType": message.SenderUserType, - "Email": message.SenderEmail, - "Message": message.Message, - "PreviewsPost": message.PreviewsPost, - "UpdateTime": TimestampConvert(message.UpdateAt), - "UpdateType": message.UpdateType, - "EditedNewMsgId": message.EditedNewMsgId, - }, - } - - return t.RenderToString("globalrelay_compliance_export_message", data) -} diff --git a/server/enterprise/message_export/main_test.go b/server/enterprise/message_export/main_test.go deleted file mode 100644 index 794f0ca457b..00000000000 --- a/server/enterprise/message_export/main_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/channels/api4" - "github.com/mattermost/mattermost/server/v8/channels/testlib" -) - -var mainHelper *testlib.MainHelper - -func TestMain(m *testing.M) { - mainHelper = testlib.NewMainHelper() - defer mainHelper.Close() - api4.SetMainHelper(mainHelper) - - mainHelper.Main(m) -} diff --git a/server/enterprise/message_export/membership_map.go b/server/enterprise/message_export/membership_map.go deleted file mode 100644 index 26dcb898972..00000000000 --- a/server/enterprise/message_export/membership_map.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -type MembershipMapUser struct { - userId string - email string - username string -} - -// Provides a clean interface for tracking the users that are present in any number of channels by channel id and user email -type MembershipMap map[string]map[string]MembershipMapUser - -func (m *MembershipMap) init(channelId string) { - if *m == nil { - *m = make(map[string]map[string]MembershipMapUser) - } - if (*m)[channelId] == nil { - (*m)[channelId] = make(map[string]MembershipMapUser) - } -} - -func (m *MembershipMap) AddUserToChannel(channelId string, user MembershipMapUser) { - m.init(channelId) - if !m.IsUserInChannel(channelId, user.email) { - (*m)[channelId][user.email] = user - } -} - -func (m *MembershipMap) RemoveUserFromChannel(channelId string, userEmail string) { - m.init(channelId) - delete((*m)[channelId], userEmail) -} - -func (m *MembershipMap) IsUserInChannel(channelId string, userEmail string) bool { - m.init(channelId) - _, exists := (*m)[channelId][userEmail] - return exists -} - -func (m *MembershipMap) GetUserEmailsInChannel(channelId string) []string { - m.init(channelId) - users := make([]string, 0, len((*m)[channelId])) - for k := range (*m)[channelId] { - users = append(users, k) - } - return users -} - -func (m *MembershipMap) GetUsersInChannel(channelId string) []MembershipMapUser { - m.init(channelId) - users := make([]MembershipMapUser, 0, len((*m)[channelId])) - for _, v := range (*m)[channelId] { - users = append(users, v) - } - return users -} diff --git a/server/enterprise/message_export/membership_map_test.go b/server/enterprise/message_export/membership_map_test.go deleted file mode 100644 index 05670d63575..00000000000 --- a/server/enterprise/message_export/membership_map_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/public/model" -) - -func TestMembershipMap(t *testing.T) { - membershipMap := make(MembershipMap) - - channelId := model.NewId() - - user1 := &MembershipMapUser{ - email: model.NewId() + "@mattermost.com", - username: model.NewId(), - userId: model.NewId(), - } - user2 := &MembershipMapUser{ - email: model.NewId() + "@mattermost.com", - username: model.NewId(), - userId: model.NewId(), - } - - assert.False(t, membershipMap.IsUserInChannel(channelId, user1.email)) - membershipMap.AddUserToChannel(channelId, *user1) - assert.True(t, membershipMap.IsUserInChannel(channelId, user1.email)) - - assert.False(t, membershipMap.IsUserInChannel(channelId, user2.email)) - membershipMap.AddUserToChannel(channelId, *user2) - assert.True(t, membershipMap.IsUserInChannel(channelId, user2.email)) - - // ensure that the correct user emails are returned - emails := membershipMap.GetUserEmailsInChannel(channelId) - assert.Len(t, emails, 2) - assert.Contains(t, emails, user1.email) - assert.Contains(t, emails, user2.email) - - // ensure that the correct user objects are returned - users := membershipMap.GetUsersInChannel(channelId) - assert.Len(t, users, 2) - if users[0].userId == user1.userId { - assert.Equal(t, user1.username, users[0].username) - assert.Equal(t, user1.email, users[0].email) - assert.Equal(t, user2.userId, users[1].userId) - assert.Equal(t, user2.username, users[1].username) - assert.Equal(t, user2.email, users[1].email) - } else if users[0].userId == user2.userId { - assert.Equal(t, user2.username, users[0].username) - assert.Equal(t, user2.email, users[0].email) - assert.Equal(t, user1.userId, users[1].userId) - assert.Equal(t, user1.username, users[1].username) - assert.Equal(t, user1.email, users[1].email) - } else { - assert.Fail(t, "First returned user is not recognized") - } - - // remove user1 from the channel - membershipMap.RemoveUserFromChannel(channelId, user1.email) - assert.False(t, membershipMap.IsUserInChannel(channelId, user1.email)) - assert.True(t, membershipMap.IsUserInChannel(channelId, user2.email)) - - // ensure that user2's email is returned - emails = membershipMap.GetUserEmailsInChannel(channelId) - assert.Len(t, emails, 1) - assert.Contains(t, emails, user2.email) - - // ensure that only user2 is returned - users = membershipMap.GetUsersInChannel(channelId) - assert.Len(t, users, 1) - assert.Equal(t, user2.userId, users[0].userId) - assert.Equal(t, user2.username, users[0].username) - assert.Equal(t, user2.email, users[0].email) -} diff --git a/server/enterprise/message_export/message_export.go b/server/enterprise/message_export/message_export.go deleted file mode 100644 index b124d863208..00000000000 --- a/server/enterprise/message_export/message_export.go +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "encoding/json" - "errors" - "strconv" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/einterfaces" - ejobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/actiance_export" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/csv_export" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/global_relay_export" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" -) - -const GlobalRelayExportFilename = "global-relay.zip" - -type MessageExportInterfaceImpl struct { - Server *app.Server -} - -type MessageExportJobInterfaceImpl struct { - Server *app.Server -} - -func init() { - app.RegisterJobsMessageExportJobInterface(func(s *app.Server) ejobs.MessageExportJobInterface { - return &MessageExportJobInterfaceImpl{s} - }) - app.RegisterMessageExportInterface(func(app *app.App) einterfaces.MessageExportInterface { - return &MessageExportInterfaceImpl{app.Srv()} - }) -} - -func (m *MessageExportInterfaceImpl) StartSynchronizeJob(rctx request.CTX, exportFromTimestamp int64) (*model.Job, *model.AppError) { - // if a valid export time was specified, put it in the job data - jobData := make(map[string]string) - if exportFromTimestamp >= 0 { - jobData[shared.JobDataBatchStartTime] = strconv.FormatInt(exportFromTimestamp, 10) - } - - // passing nil for job data will cause the worker to inherit start time from previously successful job - job, err := m.Server.Jobs.CreateJob(rctx, model.JobTypeMessageExport, jobData) - if err != nil { - return nil, err - } - - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - for job.Status == model.JobStatusPending || - job.Status == model.JobStatusInProgress || - job.Status == model.JobStatusCancelRequested { - select { - case <-ticker.C: - job, err = m.Server.Jobs.GetJob(rctx, job.Id) - if err != nil { - return nil, err - } - case <-rctx.Context().Done(): - return nil, model.NewAppError("StartSynchronizeJob", "ent.jobs.start_synchronize_job.timeout", nil, "", 0).Wrap(rctx.Context().Err()) - } - } - - return job, nil -} - -func RunBatch(rctx request.CTX, data shared.JobData, params shared.BackendParams) (shared.RunExportResults, shared.JobData, error) { - start := time.Now() - var err error - var res shared.RunExportResults - data, err = GetDataForBatch(rctx, data, params) - if err != nil { - return res, data, err - } - - if data.Finished { - return res, data, nil - } - - // Now write the data to the export type. - res, err = RunExportByType(rctx, DataToExportParams(data), params) - if err != nil { - return res, data, err - } - - data.ProcessingPostsMs = append(data.ProcessingPostsMs, res.ProcessingPostsMs) - data.ProcessingXmlMs = append(data.ProcessingXmlMs, res.ProcessingXmlMs) - data.TransferringFilesMs = append(data.TransferringFilesMs, res.TransferringFilesMs) - data.TransferringZipMs = append(data.TransferringZipMs, res.TransferringZipMs) - data.TotalBatchMs = append(data.TotalBatchMs, time.Since(start).Milliseconds()) - data.WarningCount += res.NumWarnings - data.BatchStartTime = data.BatchEndTime - - return res, data, err -} - -// GetDataForBatch gets the posts for this batch and updates JobData with the current state. -func GetDataForBatch(rctx request.CTX, data shared.JobData, params shared.BackendParams) (shared.JobData, error) { - start := time.Now() - var err error - // Using BatchSize+1 is a trick to test whether or not we've reached the final batch. - data.PostsToExport, data.Cursor, err = params.Store.Compliance().MessageExport(rctx, data.Cursor, data.BatchSize+1) - if err != nil { - return data, err - } - data.MessageExportMs = append(data.MessageExportMs, time.Since(start).Milliseconds()) - - if len(data.PostsToExport) == data.BatchSize+1 { - // We still have posts after this current batch. - // Remove the last post, we have to leave it for the next batch. - lastPostIdx := len(data.PostsToExport) - 1 - data.PostsToExport = data.PostsToExport[:lastPostIdx] - lastPostIdx = len(data.PostsToExport) - 1 - data.Cursor.LastPostUpdateAt = *data.PostsToExport[lastPostIdx].PostUpdateAt - data.Cursor.LastPostId = *data.PostsToExport[lastPostIdx].PostId - data.BatchEndTime = data.Cursor.LastPostUpdateAt - } else { - // We've reached the final batch; we need to include all join/leave events that occur after the lastpost. - // This will let us also pick up the joins/leaves that occur after lastPostUpdateAt but before JobEndTime. - data.BatchEndTime = data.JobEndTime - } - - if len(data.PostsToExport) == 0 { - data.Finished = true - return data, nil - } - - rctx.Logger().Debug("Found posts to export", mlog.Int("num_posts", len(data.PostsToExport))) - data.MessagesExported += len(data.PostsToExport) - data.BatchNumber++ - data.BatchPath = shared.GetBatchPath(data.ExportDir, data.BatchStartTime, data.BatchEndTime, data.BatchNumber) - - return data, nil -} - -type ExportParams struct { - ExportType string - ChannelMetadata map[string]*shared.MetadataChannel - ChannelMemberHistories map[string][]*model.ChannelMemberHistoryResult - PostsToExport []*model.MessageExport - JobStartTime int64 - BatchPath string - BatchStartTime int64 - BatchEndTime int64 -} - -func DataToExportParams(data shared.JobData) ExportParams { - return ExportParams{ - ExportType: data.ExportType, - ChannelMetadata: data.ChannelMetadata, - ChannelMemberHistories: data.ChannelMemberHistories, - PostsToExport: data.PostsToExport, - JobStartTime: data.JobStartTime, - BatchPath: data.BatchPath, - BatchStartTime: data.BatchStartTime, - BatchEndTime: data.BatchEndTime, - } -} - -func RunExportByType(rctx request.CTX, p ExportParams, b shared.BackendParams) (results shared.RunExportResults, err error) { - preparePosts(rctx, p.PostsToExport) - - exportParams := shared.ExportParams{ - ExportType: p.ExportType, - ChannelMetadata: p.ChannelMetadata, - Posts: p.PostsToExport, - ChannelMemberHistories: p.ChannelMemberHistories, - JobStartTime: p.JobStartTime, - BatchPath: p.BatchPath, - BatchStartTime: p.BatchStartTime, - BatchEndTime: p.BatchEndTime, - Config: b.Config, - Db: b.Store, - FileAttachmentBackend: b.FileAttachmentBackend, - ExportBackend: b.ExportBackend, - Templates: b.HtmlTemplates, - } - - switch p.ExportType { - case model.ComplianceExportTypeCsv: - rctx.Logger().Debug("Exporting CSV") - return csv_export.CsvExport(rctx, exportParams) - - case model.ComplianceExportTypeActiance: - rctx.Logger().Debug("Exporting Actiance") - return actiance_export.ActianceExport(rctx, exportParams) - - case model.ComplianceExportTypeGlobalrelay, model.ComplianceExportTypeGlobalrelayZip: - rctx.Logger().Debug("Exporting GlobalRelay") - return global_relay_export.GlobalRelayExport(rctx, exportParams) - - default: - return results, errors.New("Unknown output format: " + p.ExportType) - } -} - -func preparePosts(rctx request.CTX, postsToExport []*model.MessageExport) { - // go through all the posts and if the post's props contain 'from_bot' - override the IsBot field, since it's possible that the sender is not a user, but was a Bot and vise-versa - for _, post := range postsToExport { - if post.PostProps != nil { - props := map[string]any{} - - if json.Unmarshal([]byte(*post.PostProps), &props) == nil { - if val, ok := props["from_bot"]; ok { - post.IsBot = val == "true" - } - } - } - - // Team info can be null for DM/GM channels. - if post.TeamId == nil { - post.TeamId = new(string) - } - if post.TeamName == nil { - post.TeamName = new(string) - } - if post.TeamDisplayName == nil { - post.TeamDisplayName = new(string) - } - - // make sure user information is present. Set defaults and log an error otherwise. - if post.ChannelId == nil { - rctx.Logger().Warn("ChannelId is missing for post", mlog.String("post_id", *post.PostId)) - post.ChannelId = new(string) - } - if post.ChannelName == nil { - rctx.Logger().Warn("ChannelName is missing for post", mlog.String("post_id", *post.PostId)) - post.ChannelName = new(string) - } - if post.ChannelDisplayName == nil { - rctx.Logger().Warn("ChannelDisplayName is missing for post", mlog.String("post_id", *post.PostId)) - post.ChannelDisplayName = new(string) - } - if post.ChannelType == nil { - rctx.Logger().Warn("ChannelType is missing for post", mlog.String("post_id", *post.PostId)) - post.ChannelType = new(model.ChannelType) - } - - if post.UserId == nil { - rctx.Logger().Warn("UserId is missing for post", mlog.String("post_id", *post.PostId)) - post.UserId = new(string) - } - if post.UserEmail == nil { - rctx.Logger().Warn("UserEmail is missing for post", mlog.String("post_id", *post.PostId)) - post.UserEmail = new(string) - } - if post.Username == nil { - rctx.Logger().Warn("Username is missing for post", mlog.String("post_id", *post.PostId)) - post.Username = new(string) - } - - if post.PostType == nil { - rctx.Logger().Warn("Type is missing for post", mlog.String("post_id", *post.PostId)) - post.PostType = new(string) - } - if post.PostMessage == nil { - rctx.Logger().Warn("Message is missing for post", mlog.String("post_id", *post.PostId)) - post.PostMessage = new(string) - } - if post.PostCreateAt == nil { - rctx.Logger().Warn("CreateAt is missing for post", mlog.String("post_id", *post.PostId)) - post.PostCreateAt = new(int64) - } - } -} diff --git a/server/enterprise/message_export/message_export_test.go b/server/enterprise/message_export/message_export_test.go deleted file mode 100644 index 8ef2103e178..00000000000 --- a/server/enterprise/message_export/message_export_test.go +++ /dev/null @@ -1,2601 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "archive/zip" - "bytes" - _ "embed" - "encoding/xml" - "fmt" - "io" - "os" - "path" - "slices" - "strconv" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - st "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/actiance_export" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/global_relay_export" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -//go:embed testdata/actianceXMLHeader.tmpl -var actianceXMLHeader string - -//go:embed testdata/actianceE2E1Batch1ch2.tmpl -var actianceE2E1Batch1ch2tmpl string - -//go:embed testdata/actianceE2E1Batch1ch3.tmpl -var actianceE2E1Batch1ch3tmpl string - -//go:embed testdata/actianceE2E1Batch2.tmpl -var actianceE2E1Batch2tmpl string - -//go:embed testdata/actianceE2E1Batch3ch2.tmpl -var actianceE2E1Batch3ch2tmpl string - -//go:embed testdata/actianceE2E1Batch3ch4.tmpl -var actianceE2E1Batch3ch4tmpl string - -//go:embed testdata/actianceE2E2.tmpl -var actianceE2E2 string - -//go:embed testdata/grE2E1Batch1Summary.tmpl -var grE2E1Batch1Summary string - -//go:embed testdata/grE2E1Batch1.tmpl -var grE2E1Batch1 string - -//go:embed testdata/grE2E1Batch1SummaryCh3.tmpl -var grE2E1Batch1SummaryCh3 string - -//go:embed testdata/grE2E1Batch1Ch3.tmpl -var grE2E1Batch1Ch3 string - -//go:embed testdata/grE2E1Batch2Summary.tmpl -var grE2E1Batch2Summary string - -//go:embed testdata/grE2E1Batch2.tmpl -var grE2E1Batch2 string - -//go:embed testdata/grE2E1Batch3Summary.tmpl -var grE2E1Batch3Summary string - -//go:embed testdata/grE2E1Batch3.tmpl -var grE2E1Batch3 string - -//go:embed testdata/grE2E1Batch3SummaryCh4.tmpl -var grE2E1Batch3SummaryCh4 string - -//go:embed testdata/grE2E1Batch3Ch4.tmpl -var grE2E1Batch3Ch4 string - -//go:embed testdata/csvE2E1Batch1.tmpl -var csvE2E1Batch1 string - -//go:embed testdata/csvE2E1Batch2.tmpl -var csvE2E1Batch2 string - -//go:embed testdata/csvE2E1Batch3.tmpl -var csvE2E1Batch3 string - -//go:embed testdata/grE2E2Batch1Summary.tmpl -var grE2E2Batch1Summary string - -//go:embed testdata/grE2E2Batch1.tmpl -var grE2E2Batch1 string - -//go:embed testdata/csvE2E2Batch1.tmpl -var csvE2E2Batch1 string - -//go:embed testdata/grE2E3Batch1SummaryPerm1.tmpl -var grE2E3Batch1SummaryPerm1 string - -//go:embed testdata/grE2E3Batch1SummaryPerm2.tmpl -var grE2E3Batch1SummaryPerm2 string - -//go:embed testdata/grE2E3Batch1SummaryPerm3.tmpl -var grE2E3Batch1SummaryPerm3 string - -//go:embed testdata/grE2E3Batch1SummaryPerm4.tmpl -var grE2E3Batch1SummaryPerm4 string - -//go:embed testdata/grE2E3Batch1Perm1.tmpl -var grE2E3Batch1Perm1 string - -//go:embed testdata/grE2E3Batch1Perm2.tmpl -var grE2E3Batch1Perm2 string - -//go:embed testdata/grE2E3Batch1Perm3.tmpl -var grE2E3Batch1Perm3 string - -//go:embed testdata/grE2E3Batch1Perm4.tmpl -var grE2E3Batch1Perm4 string - -//go:embed testdata/grE2E3Batch2SummaryPerm1.tmpl -var grE2E3Batch2SummaryPerm1 string - -//go:embed testdata/grE2E3Batch2SummaryPerm2.tmpl -var grE2E3Batch2SummaryPerm2 string - -//go:embed testdata/grE2E3Batch2Perm1.tmpl -var grE2E3Batch2Perm1 string - -//go:embed testdata/grE2E3Batch2Perm2.tmpl -var grE2E3Batch2Perm2 string - -//go:embed testdata/grE2E4Summary.tmpl -var grE2E4Summary string - -//go:embed testdata/csvE2E3Batch1Perm1.tmpl -var csvE2E3Batch1Perm1 string - -//go:embed testdata/csvE2E3Batch1Perm2.tmpl -var csvE2E3Batch1Perm2 string - -//go:embed testdata/csvE2E3Batch1Perm3.tmpl -var csvE2E3Batch1Perm3 string - -//go:embed testdata/csvE2E3Batch1Perm4.tmpl -var csvE2E3Batch1Perm4 string - -//go:embed testdata/csvE2E3Batch2Perm1.tmpl -var csvE2E3Batch2Perm1 string - -//go:embed testdata/csvE2E3Batch2Perm2.tmpl -var csvE2E3Batch2Perm2 string - -//go:embed testdata/csvE2E4Batch1.tmpl -var csvE2E4Batch1 string - -func conv(dateTime int64) string { - return global_relay_export.TimestampConvert(dateTime) -} - -type MyReporter struct { - mock.Mock -} - -func (mr *MyReporter) ReportProgressMessage(message string) { - mr.Called(message) -} - -func TestRunExportByType(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - testRunExportByType(t, fileBackend, exportTempDir, fileBackend, exportTempDir) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - require.NoError(t, err) - - testRunExportByType(t, exportBackend, exportTempDir, attachmentBackend, attachmentTempDir) - }) -} - -func testRunExportByType(t *testing.T, exportBackend filestore.FileBackend, exportDir string, attachmentBackend filestore.FileBackend, attachmentDir string) { - rctx := request.TestContext(t) - - chanTypeDirect := model.ChannelTypeDirect - - t.Run("missing user info", func(t *testing.T) { - t.Cleanup(func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }) - - posts := []*model.MessageExport{ - { - PostId: model.NewPointer("post-id"), - PostOriginalId: model.NewPointer("post-original-id"), - TeamId: model.NewPointer("team-id"), - TeamName: model.NewPointer("team-name"), - TeamDisplayName: model.NewPointer("team-display-name"), - ChannelId: model.NewPointer("channel-id"), - ChannelName: model.NewPointer("channel-name"), - ChannelDisplayName: model.NewPointer("channel-display-name"), - PostCreateAt: model.NewPointer(int64(1)), - PostUpdateAt: model.NewPointer(int64(1)), - PostMessage: model.NewPointer("message"), - UserEmail: model.NewPointer("test@example.com"), - Username: model.NewPointer("Mr. Test"), - UserId: model.NewPointer(st.NewTestID()), - ChannelType: &chanTypeDirect, - PostFileIds: []string{}, - }, - } - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", int64(1), int64(1)). - Return([]string{"channel-id"}, nil) - mockStore.ChannelStore.On("GetMany", []string{"channel-id"}, true). - Return(model.ChannelList{{ - Id: "channel-id", - DisplayName: "channel-display-name", - Name: "channel-name", - Type: chanTypeDirect, - }}, nil) - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", int64(1), int64(1), []string{"channel-id"}).Return([]*model.ChannelMemberHistoryResult{}, nil) - - myMockReporter := MyReporter{} - defer myMockReporter.AssertExpectations(t) - myMockReporter.On("ReportProgressMessage", "Exporting channel information for 1 channels.") - myMockReporter.On("ReportProgressMessage", "Calculating channel activity: 0/1 channels completed.") - - channelMetadata, channelMemberHistories, err := shared.CalculateChannelExports(rctx, - shared.ChannelExportsParams{ - Store: shared.NewMessageExportStore(mockStore), - ExportPeriodStartTime: 1, - ExportPeriodEndTime: 1, - ChannelBatchSize: 100, - ChannelHistoryBatchSize: 100, - ReportProgressMessage: myMockReporter.ReportProgressMessage, - }) - assert.NoError(t, err) - - res, err := RunExportByType(rctx, ExportParams{ - ExportType: model.ComplianceExportTypeActiance, - ChannelMetadata: channelMetadata, - ChannelMemberHistories: channelMemberHistories, - PostsToExport: posts, - BatchPath: "testZipName", - BatchStartTime: 1, - BatchEndTime: 1, - }, shared.BackendParams{ - Store: shared.NewMessageExportStore(mockStore), - FileAttachmentBackend: attachmentBackend, - ExportBackend: exportBackend, - HtmlTemplates: nil, - Config: nil, - }) - require.NoError(t, err) - require.Zero(t, res.NumWarnings) - }) -} - -func TestRunExportJobE2EByType(t *testing.T) { - t.Run("no dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - testRunExportJobE2E(t, fileBackend, exportTempDir, fileBackend, exportTempDir) - }) - - t.Run("using dedicated export filestore", func(t *testing.T) { - exportTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - exportBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: exportTempDir, - }) - assert.NoError(t, err) - - attachmentTempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - attachmentBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{ - DriverName: model.ImageDriverLocal, - Directory: attachmentTempDir, - }) - require.NoError(t, err) - - testRunExportJobE2E(t, exportBackend, exportTempDir, attachmentBackend, attachmentTempDir) - }) -} - -func testRunExportJobE2E(t *testing.T, exportBackend filestore.FileBackend, exportDir string, - attachmentBackend filestore.FileBackend, attachmentDir string) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - t.Run("conflicting timestamps", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - time.Sleep(1 * time.Millisecond) - now := model.GetMillis() - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.ExportFromTimestamp = now - 1 - *cfg.MessageExportSettings.BatchSize = 2 - }) - - for range 3 { - _, err2 := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now, - }) - require.NoError(t, err2) - } - - job := runJobForTest(t, th, nil) - - warnings, err := strconv.Atoi(job.Data[shared.JobDataWarningCount]) - require.NoError(t, err) - require.Equal(t, 0, warnings) - - numExported, err := strconv.ParseInt(job.Data[shared.JobDataMessagesExported], 0, 64) - require.NoError(t, err) - require.Equal(t, int64(3), numExported) - }) - - t.Run("actiance -- multiple batches, 1 zip per batch, output to a single directory", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - res := generateActianceBatchTest1(t, th, attachmentDir, exportDir, attachmentBackend) - - files, err := exportBackend.ListDirectory(res.jobExportDir) - require.NoError(t, err) - require.ElementsMatch(t, res.batches, files) - - fileContents := openZipAndReadFile(t, exportBackend, res.batches[0], res.attachments[0].Path) - - require.EqualValuesf(t, res.contents[0], fileContents, "file contents not equal") - }) - - t.Run("actiance -- multiple batches, using UntilUpdateAt", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - res := generateActianceBatchTest2(t, th, attachmentDir, exportDir) - - files, err := exportBackend.ListDirectory(res.jobExportDir) - require.NoError(t, err) - require.ElementsMatch(t, res.batches, files) - }) - - t.Run("actiance e2e 1", func(t *testing.T) { - tests := []struct { - name string - testStopping bool - }{ - { - name: "full tests, no stopping", - testStopping: false, - }, - { - name: "full tests, stopped and resumed", - // This uses the same output as the previous e2e test, but tests that the job can be stopped and - // resumed with no change to the directory, files, file contents, or job.Data that shouldn't change. - // We want to be confident that jobs can resume without data missing or added from the original run. - testStopping: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType1Results(t, th, model.ComplianceExportTypeActiance, attachmentDir, - exportDir, attachmentBackend, exportBackend, tt.testStopping) - channel2 := ret.channels[0] - channel3 := ret.channels[1] - channel4 := ret.channels[2] - start := ret.start - jl := ret.joinLeaves - posts := ret.posts - createUpdateTimes := ret.createUpdateTimes - attachments := ret.attachments - contents := ret.contents - jobEndTime := ret.jobEndTime - batches := ret.batches - - // Expected data: - // - batch1 has two channels, and we're not sure which will come first. What a pain. - - // actiance exports, for batch 1: - // for ch2: - // 3 participants entered - // message 0 - // file for message 0 (start and ended) - // message 1 - // file for message 1 (start and ended) - // message 2 - // file for message 2 (start and ended) - // 3 participants left - // for ch3: - // 1 participant entered - // 1 participant left - - // actiance exports, for batch 2: - // for ch2: - // 2 participants entered - // message 3 - // file for message 3 (start and ended) - // message 4 - // file for message 4 (start and ended) - // message 5 - // file for message 5 (start and ended) - // 2 participants left - - // actiance exports, for batch 3: - // for ch2: - // 3 participants entered - // message 6 - // file for message 6 (start and ended) - // message 7 - // file for message 7 (start and ended) - // message 8 - // file for message 8 (start and ended) - // 3 participants left - // for ch4: - // 1 participant entered - // 1 participant left - - batch1ch2 := fmt.Sprintf(actianceE2E1Batch1ch2tmpl, channel2.Id, start, jl[0].join, jl[1].join, jl[2].join, - posts[0].Id, createUpdateTimes[0], createUpdateTimes[0], createUpdateTimes[0], - posts[1].Id, createUpdateTimes[1], createUpdateTimes[1], createUpdateTimes[1], - posts[2].Id, createUpdateTimes[2], createUpdateTimes[2], createUpdateTimes[2], - jl[1].leave, jl[2].leave, createUpdateTimes[2], createUpdateTimes[2]) - - batch1ch3 := fmt.Sprintf(actianceE2E1Batch1ch3tmpl, channel3.Id, start, jl[6].join, jl[6].leave, createUpdateTimes[2]) - - xmlHeader := strings.TrimSpace(actianceXMLHeader) - batch1Possibility1 := fmt.Sprintf(xmlHeader, batch1ch2, batch1ch3) - batch1Possibility2 := fmt.Sprintf(xmlHeader, batch1ch3, batch1ch2) - - batch2xml := fmt.Sprintf(actianceE2E1Batch2tmpl, channel2.Id, createUpdateTimes[2], jl[0].join, jl[3].join, - posts[3].Id, createUpdateTimes[3], createUpdateTimes[3], createUpdateTimes[3], - posts[4].Id, createUpdateTimes[4], createUpdateTimes[4], createUpdateTimes[4], - posts[5].Id, createUpdateTimes[5], createUpdateTimes[5], createUpdateTimes[5], - jl[3].leave, createUpdateTimes[5], createUpdateTimes[5]) - batch2xml = strings.TrimSpace(batch2xml) - - // Batch3 has two channels, and we're not sure which will come first. What a pain. - batch3ch2 := fmt.Sprintf(actianceE2E1Batch3ch2tmpl, channel2.Id, createUpdateTimes[5], jl[0].join, jl[4].join, jl[5].join, - posts[6].Id, createUpdateTimes[6], createUpdateTimes[6], createUpdateTimes[6], - posts[7].Id, createUpdateTimes[7], createUpdateTimes[7], createUpdateTimes[7], - posts[8].Id, createUpdateTimes[8], createUpdateTimes[8], createUpdateTimes[8], - jl[4].leave, jl[5].leave, jobEndTime, jobEndTime) - - batch3ch4 := fmt.Sprintf(actianceE2E1Batch3ch4tmpl, channel4.Id, createUpdateTimes[5], jl[7].join, jobEndTime, jobEndTime) - - batch3Possibility1 := fmt.Sprintf(xmlHeader, batch3ch2, batch3ch4) - - batch3Possibility2 := fmt.Sprintf(xmlHeader, batch3ch4, batch3ch2) - - for b, batchName := range batches { - xmlContents := openZipAndReadFile(t, exportBackend, batchName, "actiance_export.xml") - - // this is so clunky, sorry. but it's simple. - if b == 0 { - if xmlContents != batch1Possibility1 && xmlContents != batch1Possibility2 { - // to make some output - assert.Equal(t, batch1Possibility1, xmlContents, "batch 1 possibility 1") - assert.Equal(t, batch1Possibility2, xmlContents, "batch 1 possibility 2") - } - } - - if b == 1 { - require.Equal(t, batch2xml, xmlContents, "batch 2") - } - - if b == 2 { - if xmlContents != batch3Possibility1 && xmlContents != batch3Possibility2 { - // to make some output - assert.Equal(t, batch3Possibility1, xmlContents, "batch 3 possibility 1") - assert.Equal(t, batch3Possibility2, xmlContents, "batch 3 possibility 2") - } - } - - zipBytes, err := exportBackend.ReadFile(batchName) - require.NoError(t, err) - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - for i := range 3 { - num := b*3 + i - attachmentInZip, err := zipReader.Open(attachments[num].Path) - require.NoError(t, err) - attachmentInZipContents, err := io.ReadAll(attachmentInZip) - require.NoError(t, err) - err = attachmentInZip.Close() - require.NoError(t, err) - require.EqualValuesf(t, contents[num], string(attachmentInZipContents), "file contents not equal") - } - } - }) - } - }) - - t.Run("GlobalRelay e2e 1", func(t *testing.T) { - tests := []struct { - name string - testStopping bool - }{ - { - name: "full tests, no stopping", - testStopping: false, - }, - { - name: "full tests, stopped and resumed", - // This uses the same output as the previous e2e test, but tests that the job can be stopped and - // resumed with no change to the directory, files, file contents, or job.Data that shouldn't change. - // We want to be confident that jobs can resume without data missing or added from the original run. - testStopping: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType1Results(t, th, model.ComplianceExportTypeGlobalrelayZip, attachmentDir, - exportDir, attachmentBackend, exportBackend, tt.testStopping) - teams := ret.teams - channel2 := ret.channels[0] - channel3 := ret.channels[1] - channel4 := ret.channels[2] - users := ret.users - posts := ret.posts - batchTimes := ret.batchTimes - jobStartTime := ret.start - jl := ret.joinLeaves - batches := ret.batches - cu := ret.createUpdateTimes - - // aligned with actiance exports, for batch 1: - // ** except that there is no closed-out leaves at batch end - // for ch2: - // 3 participants entered (1, 2, 3) - // message 0 - // file for message 0 (start and ended) - // message 1 - // file for message 1 (start and ended) - // message 2 - // file for message 2 (start and ended) - // 1 participants left - // for ch3: - // 1 participant entered - // 1 participant left - - // for batch 2: - // for ch2: - // 2 participants entered - // message 3 - // file for message 3 (start and ended) - // message 4 - // file for message 4 (start and ended) - // message 5 - // file for message 5 (start and ended) - // 1 participant left - - // for batch 3: - // for ch2: - // 3 participants entered - // message 6 - // file for message 6 (start and ended) - // message 7 - // file for message 7 (start and ended) - // message 8 - // file for message 8 (start and ended) - // 2 participants left - // for ch4: - // 1 participant entered - - for batchNum, batchName := range batches { - data1 := openZipAndReadFileStartingWith(t, exportBackend, batchName, channel2.Name) - // clean some bad csrf if present - msg1 := global_relay_export.CleanTestOutput(data1) - - batchStartTime := batchTimes[batchNum].start - batchEndTime := batchTimes[batchNum].end - expectedBatchExportCh2 := []string{ - // batch 1 Summary - fmt.Sprintf(grE2E1Batch1Summary, - // 1 2 3 4 5 - channel2.DisplayName, conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 6 7 8 9 - conv(jl[1].join), conv(jl[1].leave), conv(jl[2].join), conv(jl[2].leave), - // 10 11 12 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), - // 13 14 15 16 - channel2.Id, channel2.TeamId, teams[0].Name, teams[0].DisplayName, - // 17 18 19 20 21 22 - users[0].Id, users[1].Id, users[2].Id, posts[0].Id, posts[1].Id, posts[2].Id), - - // batch 1 - fmt.Sprintf(grE2E1Batch1, - // 1 2 3 4 5 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), conv(jl[1].join), - // 6 7 8 9 10 - conv(jl[1].leave), conv(jl[2].join), conv(jl[2].leave), conv(cu[0]), conv(cu[1]), - // 11 12 13 14 15 - conv(cu[2]), conv(jobStartTime), teams[0].Id, teams[0].Name, teams[0].DisplayName, - // 16 17 18 19 20 21 - users[0].Id, users[1].Id, users[2].Id, posts[0].Id, posts[1].Id, posts[2].Id, - // 22 - channel2.Id), - - // batch 2 Summary - fmt.Sprintf(grE2E1Batch2Summary, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(jl[3].join), conv(jl[3].leave), conv(posts[3].CreateAt), conv(posts[4].CreateAt), - // 9 - conv(posts[5].CreateAt), - // 10 11 12 13 - channel2.Id, channel2.TeamId, teams[0].Name, teams[0].DisplayName, - // 14 15 16 17 18 - users[0].Id, users[3].Id, posts[3].Id, posts[4].Id, posts[5].Id, - ), - - // batch 2 - fmt.Sprintf(grE2E1Batch2, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 9 - conv(jl[3].join), conv(jl[3].leave), conv(cu[3]), conv(cu[4]), conv(cu[5]), - // 10 11 12 13 - conv(jobStartTime), teams[0].Id, teams[0].Name, teams[0].DisplayName, - // 14 15 16 17 18 19 - users[0].Id, users[1].Id, users[3].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 20 - channel2.Id), - - // batch 3 Summary - fmt.Sprintf(grE2E1Batch3Summary, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(jl[4].join), conv(jl[4].leave), conv(jl[5].join), conv(jl[5].leave), - // 9 10 11 - conv(posts[6].CreateAt), conv(posts[7].CreateAt), conv(posts[8].CreateAt), - // 12 13 14 15 - channel2.Id, channel2.TeamId, teams[0].Name, teams[0].DisplayName, - // 16 17 18 19 20 21 - users[0].Id, users[1].Id, users[2].Id, posts[6].Id, posts[7].Id, posts[8].Id, - // 22 23 - users[4].Id, users[5].Id), - - // batch 3 - fmt.Sprintf(grE2E1Batch3, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(jl[4].join), conv(jl[4].leave), conv(jl[5].join), conv(jl[5].leave), - // 9 10 11 12 - conv(cu[6]), conv(cu[7]), conv(cu[8]), conv(jobStartTime), - // 13 14 15 - teams[0].Id, teams[0].Name, teams[0].DisplayName, - // 16 17 18 19 20 21 - users[0].Id, users[4].Id, users[5].Id, posts[6].Id, posts[7].Id, posts[8].Id, - // 22 - channel2.Id), - } - - if batchNum == 0 { - global_relay_export.AssertHeaderContains(t, msg1, map[string]string{ - "Subject": "Mattermost Compliance Export: the Channel Two", - "From": "user1@email", - "X-Mattermost-ChannelName": "the Channel Two", - "To": "user1@email,user2@email,user3@email", - "X-Mattermost-ChannelID": channel2.Id, - "X-Mattermost-ChannelType": "private", - }) - assert.Contains(t, msg1, expectedBatchExportCh2[0], "batch 1 Ch2 summary") - assert.Contains(t, msg1, expectedBatchExportCh2[1], "batch 1 Ch2") - - // now read second channel's export - data2 := openZipAndReadFileStartingWith(t, exportBackend, batchName, channel3.Name) - // clean some bad csrf if present - msg2 := global_relay_export.CleanTestOutput(data2) - - expectedBatchExportCh3 := []string{ - // batch 1 Summary - fmt.Sprintf(grE2E1Batch1SummaryCh3, - // 1 2 3 4 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channel3.Id, - // 5 6 7 8 9 - conv(batchStartTime), conv(batchEndTime), users[6].Id, conv(jl[6].join), conv(jl[6].leave)), - - // batch 1 - fmt.Sprintf(grE2E1Batch1Ch3, - // 1 2 3 4 - teams[0].Id, channel3.Id, conv(batchStartTime), conv(batchEndTime), - // 5 6 7 8 - users[6].Id, conv(jl[6].join), conv(jl[6].leave), conv(jobStartTime)), - } - - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><>batch1 Ch3 actual\n\n%s\n\n<><>batch1 Ch3 Summary:\n\n%s\n\n", - // msg2, expectedBatchExportCh3[0]) - - // Channel 3 - global_relay_export.AssertHeaderContains(t, msg2, map[string]string{ - "Subject": "Mattermost Compliance Export: the Channel Three", - "From": "user7@email", - "X-Mattermost-ChannelName": "the Channel Three", - "To": "user7@email", - "X-Mattermost-ChannelID": channel3.Id, - "X-Mattermost-ChannelType": "public", - }) - assert.Contains(t, msg2, expectedBatchExportCh3[0], "batch 1 Ch3 summary") - assert.Contains(t, msg2, expectedBatchExportCh3[1], "batch 1 Ch3") - } - - if batchNum == 1 { - global_relay_export.AssertHeaderContains(t, msg1, map[string]string{ - "Subject": "Mattermost Compliance Export: the Channel Two", - "From": "user1@email", - "X-Mattermost-ChannelName": "the Channel Two", - "To": "user1@email,user4@email", - "X-Mattermost-ChannelID": channel2.Id, - "X-Mattermost-ChannelType": "private", - }) - assert.Contains(t, msg1, expectedBatchExportCh2[2], "batch 2 ch2 summary") - assert.Contains(t, msg1, expectedBatchExportCh2[3], "batch 2 ch2") - } - - if batchNum == 2 { - global_relay_export.AssertHeaderContains(t, msg1, map[string]string{ - "Subject": "Mattermost Compliance Export: the Channel Two", - "From": "user1@email", - "X-Mattermost-ChannelName": "the Channel Two", - "To": "user1@email,user5@email,user6@email", - "X-Mattermost-ChannelID": channel2.Id, - "X-Mattermost-ChannelType": "private", - }) - assert.Contains(t, msg1, expectedBatchExportCh2[4], "batch 3 ch2 summary") - assert.Contains(t, msg1, expectedBatchExportCh2[5], "batch 3 ch2") - - // now read second channel's export - data2 := openZipAndReadFileStartingWith(t, exportBackend, batchName, channel4.Name) - // clean some bad csrf if present - msg2 := global_relay_export.CleanTestOutput(data2) - - expectedBatchExportCh4 := []string{ - // batch 1 Summary - fmt.Sprintf(grE2E1Batch3SummaryCh4, - // 1 2 3 4 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channel4.Id, - // 5 6 7 8 - conv(batchStartTime), conv(batchEndTime), users[7].Id, conv(jl[7].join)), - - // batch 1 - fmt.Sprintf(grE2E1Batch3Ch4, - // 1 2 3 4 - teams[0].Id, channel4.Id, conv(batchStartTime), conv(batchEndTime), - // 5 6 7 8 - users[7].Id, conv(jl[7].join), conv(jl[7].leave), conv(jobStartTime)), - } - - // Channel 3 - global_relay_export.AssertHeaderContains(t, msg2, map[string]string{ - "Subject": "Mattermost Compliance Export: the Channel Four", - "From": "user8@email", - "X-Mattermost-ChannelName": "the Channel Four", - "To": "user8@email", - "X-Mattermost-ChannelID": channel4.Id, - "X-Mattermost-ChannelType": "public", - }) - assert.Contains(t, msg2, expectedBatchExportCh4[0], "batch 3 Ch4 summary") - assert.Contains(t, msg2, expectedBatchExportCh4[1], "batch 3 Ch4") - } - } - }) - } - }) - - t.Run("CSV e2e 1", func(t *testing.T) { - tests := []struct { - name string - testStopping bool - }{ - { - name: "full tests, no stopping", - testStopping: false, - }, - { - name: "full tests, stopped and resumed", - // This uses the same output as the previous e2e test, but tests that the job can be stopped and - // resumed with no change to the directory, files, file contents, or job.Data that shouldn't change. - // We want to be confident that jobs can resume without data missing or added from the original run. - testStopping: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType1Results(t, th, model.ComplianceExportTypeCsv, attachmentDir, - exportDir, attachmentBackend, exportBackend, tt.testStopping) - jl := ret.joinLeaves - cu := ret.createUpdateTimes - posts := ret.posts - files := ret.attachments - users := ret.users - channels := ret.channels - teams := ret.teams - batches := ret.batches - - // aligned with actiance exports, for batch 1: - // ** except that there is no closed-out leaves at batch end - // for ch2: - // 3 participants entered - // message 0 - // file for message 0 (start and ended) - // message 1 - // file for message 1 (start and ended) - // message 2 - // file for message 2 (start and ended) - // 1 participants left - // for ch3: - // 1 participant entered - // 1 participant left - - // for batch 2: - // for ch2: - // 2 participants entered - // message 3 - // file for message 3 (start and ended) - // message 4 - // file for message 4 (start and ended) - // message 5 - // file for message 5 (start and ended) - // 1 participant left - - // for batch 3: - // for ch2: - // 3 participants entered - // message 6 - // file for message 6 (start and ended) - // message 7 - // file for message 7 (start and ended) - // message 8 - // file for message 8 (start and ended) - // 2 participants left - // for ch4: - // 1 participant entered - - expectedBatchExport := []string{ - // batch 1 - fmt.Sprintf(csvE2E1Batch1, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, channels[1].Id, - // 6 7 8 9 - users[0].Id, users[1].Id, users[2].Id, users[6].Id, - // 10 11 12 13 14 15 - posts[0].Id, posts[1].Id, posts[2].Id, files[0].Id, files[1].Id, files[2].Id, - // 16 17 18 19 20 21 22 - jl[0].join, jl[1].join, jl[1].leave, cu[0], jl[2].join, cu[1], jl[2].leave, - // 23 24 25 - jl[6].join, jl[6].leave, cu[2]), - - // batch 2 - fmt.Sprintf(csvE2E1Batch2, - // 1 2 3 4 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, - // 5 6 7 8 9 - users[0].Id, users[3].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 10 11 12 - files[3].Id, files[4].Id, files[5].Id, - // 13 14 15 16 17 18 - jl[0].join, jl[3].join, jl[3].leave, cu[3], cu[4], cu[5]), - - // batch 3 - fmt.Sprintf(csvE2E1Batch3, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, channels[2].Id, - // 6 7 8 9 - users[0].Id, users[4].Id, users[5].Id, users[7].Id, - // 10 11 12 - posts[6].Id, posts[7].Id, posts[8].Id, - // 13 14 15 - files[6].Id, files[7].Id, files[8].Id, - // 16 17 18 19 20 21 - jl[0].join, jl[4].join, jl[4].leave, cu[6], cu[7], cu[8], - // 22 23 24 - jl[5].join, jl[5].leave, jl[7].join), - } - - for batchNum, batchName := range batches { - export := openZipAndReadFileNum(t, exportBackend, batchName, 0) - - exportLines := strings.Split(export, "\n") - expectedLines := strings.Split(expectedBatchExport[batchNum], "\n") - - // the export is not always sorted when there are > 1 channels, so do this: - assert.Len(t, exportLines, len(expectedLines)) - for _, l := range expectedLines { - assert.Contains(t, exportLines, l, "batch %d, batchName: %s, \nExpected:\n\n%s\n\nGot:\n\n%v\n\n", batchNum+1, batchName, l, exportLines) - } - } - }) - } - }) - - t.Run("actiance e2e 2 - post from user not in channel", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType2Results(t, th, model.ComplianceExportTypeActiance, attachmentDir, - exportDir, attachmentBackend, exportBackend) - channel2 := ret.channels[0] - start := ret.start - jl := ret.joinLeaves - posts := ret.posts - createUpdateTimes := ret.createUpdateTimes - jobEndTime := ret.jobEndTime - batches := ret.batches - - // actiance export: - // 2 participants entered - // message 1 - // message 2 - // 2 participants left - - // Expected data: - batch1xml := fmt.Sprintf(strings.TrimSpace(actianceE2E2), channel2.Id, start, jl[0].join, start, - posts[0].Id, createUpdateTimes[0], - posts[1].Id, createUpdateTimes[1], - jobEndTime, jobEndTime, jobEndTime) - - xmlContents := openZipAndReadFile(t, exportBackend, batches[0], "actiance_export.xml") - - require.Equal(t, batch1xml, xmlContents, "batch 1") - }) - - t.Run("GlobalRelay e2e 2 - post from user not in channel", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType2Results(t, th, model.ComplianceExportTypeGlobalrelayZip, attachmentDir, - exportDir, attachmentBackend, exportBackend) - posts := ret.posts - batchTimes := ret.batchTimes - jobStartTime := ret.start - jl := ret.joinLeaves - batches := ret.batches - cu := ret.createUpdateTimes - users := ret.users - channels := ret.channels - teams := ret.teams - - // to align with actiance export: - // 2 participants entered - // message 1 - // message 2 - // 2 participants left - - batchStartTime := batchTimes[0].start - batchEndTime := batchTimes[0].end - - expectedBatchExport := []string{ - // batch 1 Summary - fmt.Sprintf(grE2E2Batch1Summary, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(batchStartTime), conv(batchEndTime), conv(posts[0].CreateAt), conv(posts[1].CreateAt), - // 9 10 11 12 13 14 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, users[1].Id, - // 15 16 - posts[0].Id, posts[1].Id, - ), - - // batch 1 - fmt.Sprintf(grE2E2Batch1, - // 1 2 3 4 5 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), conv(batchStartTime), - // 6 7 8 9 - conv(batchEndTime), conv(cu[0]), conv(cu[1]), conv(jobStartTime), - // 10 11 12 13 14 15 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, users[1].Id, - // 16 17 - posts[0].Id, posts[1].Id, - ), - } - - data := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - // clean some bad csrf if present - msg := global_relay_export.CleanTestOutput(data) - - // For debugging, better keep it in case we need it again. - //t.Logf("<><>actual\n\n%s\n\n<><>batch1 Summary:\n\n%s\n\n<><>batch1:\n%s\n", msg, expectedBatchExport[0], expectedBatchExport[1]) - - assert.Contains(t, msg, expectedBatchExport[0], "batch1 summary") - assert.Contains(t, msg, expectedBatchExport[1], "batch1") - }) - - t.Run("CSV e2e 2 - post from user not in channel", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - ret := generateE2ETestType2Results(t, th, model.ComplianceExportTypeCsv, attachmentDir, - exportDir, attachmentBackend, exportBackend) - posts := ret.posts - jl := ret.joinLeaves - batches := ret.batches - batchTimes := ret.batchTimes - cu := ret.createUpdateTimes - users := ret.users - channels := ret.channels - teams := ret.teams - batchStartTime := batchTimes[0].start - - // to align with actiance export: (remember no close-out message on batch end)o - // 2 participants entered - // message 1 - // message 2 - - expectedExport := fmt.Sprintf(csvE2E2Batch1, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 11 12 - users[1].Id, posts[0].Id, posts[1].Id, jl[0].join, batchStartTime, cu[0], cu[1]) - - export := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - assert.Equal(t, expectedExport, export) - }) - - t.Run("actiance e2e 3 - test create, update, delete xml fields", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret, type3Ret := generateE2ETestType3Results(t, th, model.ComplianceExportTypeActiance, attachmentDir, exportDir, attachmentBackend, exportBackend) - batches := ret.batches - posts := ret.posts - users := ret.users - attachments := ret.attachments - contents := ret.contents - - // actiance export: - // message 0 - // message 1 - // message 1 deleted - // message 2 updated (reaction): post2 createdAt, updatedPost2 updateAt - // message 3 created - // file 3 upload start and stopped - // message 3 deleted - // file 3 deleted - // message 4 -- same update at as below - // edited message 4 -- same update at as above - // message 6 -- same update at as below - // edited message 6 -- same update at as above - - // 2 participants left - - for b, batchName := range batches { - xmlContents := openZipAndReadFile(t, exportBackend, batchName, "actiance_export.xml") - - // Because 7 and 8 fall on the boundary, they could be in either batch, so save them here - // and then test for one or the other in each batch. - // The new post that got modified by message8 - message7 := &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[6].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[6].CreateAt, - Message: posts[6].Message, // the original message - UpdateAt: posts[6].UpdateAt, // the edit update at - UpdatedType: shared.EditedOriginalMsg, - EditedNewMsgId: posts[7].Id, - } - // The old post which has been edited - message8 := &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[7].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[7].CreateAt, - Message: posts[7].Message, // edited message - UpdateAt: posts[7].UpdateAt, - UpdatedType: shared.EditedNewMsg, - } - - if b == 0 { - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><> xml contents: \n\n%s\n\n", xmlContents) - exportedChannels := actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages := exportedChannels[0].Messages - require.Len(t, messages, 10) // batch size 7 + deleted msg1, deleted ms3, 1 deleted file msg - - // message 3's deleted file was uploaded in this batch period - fileTransferStarted := exportedChannels[0].FileStarts - require.Len(t, fileTransferStarted, 1) - fileTransferStopped := exportedChannels[0].FileStops - require.Len(t, fileTransferStopped, 1) - - // 0 - post create - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: posts[0].Message, - }, messages[0]) - - // 1 - post created - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: posts[1].Message, - }, messages[1]) - - // 1 - post deleted - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: "delete " + posts[1].Message, - UpdateAt: type3Ret.message1DeleteAt, - UpdatedType: shared.Deleted, - }, messages[2]) - - // 2 - post updated not edited (e.g., reaction) - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[2].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[2].CreateAt, - Message: posts[2].Message, - UpdateAt: type3Ret.updatedPost2.UpdateAt, - UpdatedType: shared.UpdatedNoMsgChange, - }, messages[3]) - - // 3 - post created - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[3].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[3].CreateAt, - Message: posts[3].Message, - }, messages[4]) - - // 3 - post deleted with a deleted file - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[3].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[3].CreateAt, - Message: "delete " + posts[3].Message, - UpdateAt: type3Ret.message3AndFileInfoDeleteAt, - UpdatedType: shared.Deleted, - }, messages[5]) - - // file deleted message - assert.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: messages[4].MessageId, // cheating bc we don't have this messageId - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[3].CreateAt, - Message: "delete " + attachments[0].Path, - UpdateAt: type3Ret.message3AndFileInfoDeleteAt, - UpdatedType: shared.FileDeleted, - }, messages[6]) - - // the next messages 5, 6 can be in any order because all have equal `updateAt`s - // 4 - original post - equalUpdateAts := []*actiance_export.PostExport{ - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[4].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[4].CreateAt, - Message: posts[4].Message, - UpdateAt: posts[4].UpdateAt, // will be the edit update at - UpdatedType: shared.EditedOriginalMsg, - EditedNewMsgId: posts[5].Id, - }, - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[5].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[5].CreateAt, - Message: posts[5].Message, - UpdateAt: posts[5].UpdateAt, - UpdatedType: shared.EditedNewMsg, - }, - } - require.ElementsMatch(t, equalUpdateAts, []*actiance_export.PostExport{ - messages[7], messages[8]}) - require.ElementsMatch(t, []string{posts[4].Id, posts[5].Id}, []string{messages[7].MessageId, messages[8].MessageId}) - - // the last message is one of the two edited or original - if messages[9].MessageId == message7.MessageId { - assert.Equal(t, message7, messages[9]) - } else { - assert.Equal(t, message8, messages[9]) - } - - // only batch one has files, but they're deleted - zipBytes, err := exportBackend.ReadFile(batchName) - require.NoError(t, err) - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - attachmentInZip, err := zipReader.Open(attachments[0].Path) - require.NoError(t, err) - attachmentInZipContents, err := io.ReadAll(attachmentInZip) - require.NoError(t, err) - err = attachmentInZip.Close() - require.NoError(t, err) - assert.EqualValuesf(t, contents[0], string(attachmentInZipContents), "file contents not equal") - } - - if b == 1 { - exportedChannels := actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages := exportedChannels[0].Messages - require.Len(t, messages, 1) - - // check for either message 7 or message8 - if messages[0].MessageId == message7.MessageId { - assert.Equal(t, message7, messages[0]) - } else { - assert.Equal(t, message8, messages[0]) - } - } - } - }) - - t.Run("GlobalRelay e2e 3 - test create, update, delete fields", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret, type3Ret := generateE2ETestType3Results(t, th, model.ComplianceExportTypeGlobalrelayZip, attachmentDir, - exportDir, attachmentBackend, exportBackend) - jl := ret.joinLeaves - posts := ret.posts - batchTimes := ret.batchTimes - jobStartTime := ret.start - batches := ret.batches - users := ret.users - channels := ret.channels - teams := ret.teams - - // to align with actiance export: - // message 0 - // message 1 - // message 1 deleted - // message 2 updated (reaction): post2 createdAt, updatedPost2 updateAt - // message 3 created - // file 3 upload start and stopped - // message 3 deleted - // file 3 deleted - // message 4 -- same update at as below - // edited message 4 -- same update at as above - // message 6 -- same update at as below - // edited message 6 -- same update at as above - - // 2 participants left - - batchStartTime := batchTimes[0].start - batchEndTime := batchTimes[0].end - - // The comments on 10, 11, 12 show the permutation -- this is needed because 10 & 11 have same UpdateAt, - // and 12 & 1 (in batch 2) have same UpdateAt - expectedBatch1Summaries := []string{ - // batch 1 Summary -- Permutation 1 - fmt.Sprintf(grE2E3Batch1SummaryPerm1, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt)), - - // batch 1 Summary -- Permutation 2 - fmt.Sprintf(grE2E3Batch1SummaryPerm2, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt)), - - // batch 1 summary - Permutation 3 - fmt.Sprintf(grE2E3Batch1SummaryPerm3, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt)), - - // batch 1 Summary -- Permutation 4 - fmt.Sprintf(grE2E3Batch1SummaryPerm4, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt)), - } - - // The comments on 10, 11, 12 show the permutation -- this is needed because 10 & 11 have same UpdateAt, - // and 12 & 1 (in batch 2) have same UpdateAt - expectedBatch1 := []string{ - // batch 1 -- Permutation 1 - fmt.Sprintf(grE2E3Batch1Perm1, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt), - ), - - // batch 1 -- Permutation 2 - fmt.Sprintf(grE2E3Batch1Perm2, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 edited 11 original - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[5].CreateAt), conv(posts[4].CreateAt), - // 12 original - conv(posts[6].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt), - ), - - // batch 1 - Permutation 3 - fmt.Sprintf(grE2E3Batch1Perm3, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 original 11 edited - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[4].CreateAt), conv(posts[5].CreateAt), - // 12 edited - conv(posts[7].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt), - ), - - // batch 1 -- Permutation 4 - fmt.Sprintf(grE2E3Batch1Perm4, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 9 10 edited 11 original - conv(type3Ret.message3AndFileInfoDeleteAt), conv(posts[5].CreateAt), conv(posts[4].CreateAt), - // 12 edited - conv(posts[7].CreateAt), - // 13 14 15 16 17 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 18 19 20 21 22 23 24 25 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, posts[6].Id, posts[7].Id, - // 26 27 28 message 4 orig/edited - conv(type3Ret.message1DeleteAt), conv(type3Ret.updatedPost2.UpdateAt), conv(posts[4].UpdateAt), - // 29 editedOriginal - conv(posts[6].UpdateAt), - ), - } - - batchStartTime = batchTimes[1].start - batchEndTime = batchTimes[1].end - - expectedBatch2Summaries := []string{ - // batch 2 Summary -- Permutation 1 - fmt.Sprintf(grE2E3Batch2SummaryPerm1, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 edited - conv(posts[7].CreateAt), - // 6 7 8 9 10 11 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, posts[0].Id, - // 12 13 14 - conv(posts[6].UpdateAt), posts[6].Id, posts[7].Id, - ), - - // batch 2 Summary -- Permutation 2 - fmt.Sprintf(grE2E3Batch2SummaryPerm2, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 original - conv(posts[6].CreateAt), - // 6 7 8 9 10 11 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, posts[7].Id, - // 12 13 14 - conv(posts[6].UpdateAt), posts[6].Id, posts[7].Id), - } - - expectedBatch2 := []string{ - // batch 2 Summary -- Permutation 1 - fmt.Sprintf(grE2E3Batch2Perm1, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 edited - conv(posts[7].CreateAt), - // 6 7 8 9 10 11 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, posts[7].Id, - // 12 13 14 - conv(posts[6].UpdateAt), posts[6].Id, posts[7].Id, - // 15 - conv(jobStartTime)), - - // batch 2 Summary -- Permutation 2 - fmt.Sprintf(grE2E3Batch2Perm2, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 original - conv(posts[6].CreateAt), - // 6 7 8 9 10 11 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, posts[7].Id, - // 12 13 14 - conv(posts[6].UpdateAt), posts[6].Id, posts[7].Id, - // 15 - conv(jobStartTime)), - } - - data := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - // clean some bad csrf if present - msg := global_relay_export.CleanTestOutput(data) - - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><>batch1 actual\n\n%s\n\n<><>batch1 Summary Perm1:\n\n%s\n\n<><>batch1 Summary Perm2:\n\n%s\n\n<><>batch1 Summary Perm3:\n\n%s\n\n<><>batch1 Summary Perm4:\n\n%s\n\n", - // msg, expectedBatch1Summaries[0], expectedBatch1Summaries[1], expectedBatch1Summaries[2], expectedBatch1Summaries[3]) - //t.Logf("<><>batch1 actual\n\n%s\n\n<><>batch1 Perm1:\n\n%s\n\n<><>batch1 Perm2:\n\n%s\n\n<><>batch1 Perm3:\n\n%s\n\n<><>batch1 Perm4:\n\n%s\n\n", - // msg, expectedBatch1[0], expectedBatch1[1], expectedBatch1[2], expectedBatch1[3]) - - matched := dataContainsOneOfExpected(msg, expectedBatch1Summaries) - - assert.True(t, matched, "batch 1 summary didn't match one of the expected permutations") - - matched = dataContainsOneOfExpected(msg, expectedBatch1) - assert.True(t, matched, "batch 1 body didn't match one of the expected permutations") - - data = openZipAndReadFileNum(t, exportBackend, batches[1], 0) - // clean some bad csrf if present - msg = global_relay_export.CleanTestOutput(data) - - // Note: for debugging, better keep this in case we need it again. - //t.Logf("<><>batch2 actual\n\n%s\n\n<><>batch2 Summary Perm1:\n\n%s\n\n<><>batch2 Summary Perm2:\n\n%s\n\n", - // msg, expectedBatch2Summaries[0], expectedBatch2Summaries[1]) - //t.Logf("<><>batch2 actual\n\n%s\n\n<><>batch2 Perm1:\n\n%s\n\n<><>batch2 Perm2:\n\n%s\n\n", - // msg, expectedBatch2[0], expectedBatch2[1]) - - matched = dataContainsOneOfExpected(msg, expectedBatch2Summaries) - assert.True(t, matched, "batch 2 summary didn't match one of the expected permutations") - - matched = dataContainsOneOfExpected(msg, expectedBatch2) - assert.True(t, matched, "batch 2 body didn't match one of the expected permutations") - }) - - t.Run("CSV e2e 3 - test create, update, delete fields", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret, ret3 := generateE2ETestType3Results(t, th, model.ComplianceExportTypeCsv, attachmentDir, - exportDir, attachmentBackend, exportBackend) - jl := ret.joinLeaves - posts := ret.posts - //cu := ret.createUpdateTimes - //jobStartTime := ret.start - batches := ret.batches - users := ret.users - channels := ret.channels - teams := ret.teams - attachments := ret.attachments - - // aligned with actiance export: - // message 0 - // message 1 - // message 1 deleted - // message 2 updated (reaction): post2 createdAt, updatedPost2 updateAt - // message 3 created - // file 3 upload start and stopped - // message 3 deleted - // file 3 deleted - // message 4 -- same update at as below - // edited message 4 -- same update at as above - // message 6 -- same update at as below - // edited message 6 -- same update at as above - - // NOTE: the comments describe the order of the last three messages (21 22 23), - // eg in perm1: message 4, edited message 4, and message 6 - expectedExports := []string{ - // original, edited, "original" msg 6 - fmt.Sprintf(csvE2E3Batch1Perm1, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 11 edited - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 12 13 - posts[6].Id, attachments[0].Id, - // 14 15 16 17 18 - jl[0].join, posts[0].CreateAt, posts[1].CreateAt, ret3.message1DeleteAt, posts[2].CreateAt, - // 19 20 21 22 23 - posts[3].CreateAt, ret3.message3AndFileInfoDeleteAt, posts[4].CreateAt, posts[4].UpdateAt, posts[6].CreateAt, - // 24 editedBy for message 6, 25 26 - posts[7].Id, ret3.updatedPost2.UpdateAt, posts[6].UpdateAt), - - // edited, original, original - fmt.Sprintf(csvE2E3Batch1Perm2, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 11 edited - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 12 13 - posts[6].Id, attachments[0].Id, - // 14 15 16 17 18 - jl[0].join, posts[0].CreateAt, posts[1].CreateAt, ret3.message1DeleteAt, posts[2].CreateAt, - // 19 20 21 22 23 - posts[3].CreateAt, ret3.message3AndFileInfoDeleteAt, posts[4].CreateAt, posts[4].UpdateAt, posts[6].CreateAt, - // 24 editedBy for message 6, 25 26 - posts[7].Id, ret3.updatedPost2.UpdateAt, posts[6].UpdateAt), - - // original, edited, edited - fmt.Sprintf(csvE2E3Batch1Perm3, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 11 edited - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 12 id of edited msg6 13 - posts[7].Id, attachments[0].Id, - // 14 15 16 17 18 - jl[0].join, posts[0].CreateAt, posts[1].CreateAt, ret3.message1DeleteAt, posts[2].CreateAt, - // 19 20 21 22 23 - posts[3].CreateAt, ret3.message3AndFileInfoDeleteAt, posts[4].CreateAt, posts[4].UpdateAt, posts[6].CreateAt, - // 24 25 26 - ret3.updatedPost2.UpdateAt, posts[6].CreateAt, posts[6].UpdateAt), - - // edited, original, edited - fmt.Sprintf(csvE2E3Batch1Perm4, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 11 edited - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, posts[5].Id, - // 12 id of edited msg6 13 - posts[7].Id, attachments[0].Id, - // 14 15 16 17 18 - jl[0].join, posts[0].CreateAt, posts[1].CreateAt, ret3.message1DeleteAt, posts[2].CreateAt, - // 19 20 21 22 - posts[3].CreateAt, ret3.message3AndFileInfoDeleteAt, posts[4].CreateAt, posts[4].UpdateAt, - // 23 24 25 26 - posts[6].CreateAt, ret3.updatedPost2.UpdateAt, posts[6].CreateAt, posts[6].UpdateAt), - } - - export := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - - matched := slices.Contains(expectedExports, export) - assert.True(t, matched, "batch 1 didn't match one of the expected permutations") - - expectedExports = []string{ - // original message 6 (which says "message 6" in the message, and has new id -- which is post 6's id) - fmt.Sprintf(csvE2E3Batch2Perm1, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 - posts[6].Id, - // 7 this is the editedBy for message 6 -- editedBy is confusing (cause it's the original id), but it is what it is - posts[7].Id, - // 8 9 10 - posts[6].CreateAt, jl[0].join, posts[6].UpdateAt), - - // edited message 6 (which says "edited message 6" in the message, and has original id) - fmt.Sprintf(csvE2E3Batch2Perm2, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 - posts[7].Id, posts[6].CreateAt, jl[0].join, posts[6].UpdateAt), - } - - export = openZipAndReadFileNum(t, exportBackend, batches[1], 0) - - matched = slices.Contains(expectedExports, export) - assert.True(t, matched, "batch 2 didn't match one of the expected permutations") - }) - - t.Run("actiance e2e 4 - test edits with multiple simultaneous updates", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret := generateE2ETestType4Results(t, th, model.ComplianceExportTypeActiance, attachmentDir, exportDir, attachmentBackend, exportBackend) - batch001 := ret.batches[0] - posts := ret.posts - users := ret.users - - xmlContents := openZipAndReadFile(t, exportBackend, batch001, "actiance_export.xml") - - exportedChannels := actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages := exportedChannels[0].Messages - require.Len(t, messages, 5) - - // the messages can be in any order because all have equal `updateAt`s - equalUpdateAts := []*actiance_export.PostExport{ - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: posts[0].Message, - UpdateAt: posts[1].UpdateAt, // the edit update at - UpdatedType: shared.EditedOriginalMsg, - EditedNewMsgId: posts[1].Id, - }, - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: posts[1].Message, - UpdateAt: posts[1].UpdateAt, - UpdatedType: shared.EditedNewMsg, - }, - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[2].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[2].CreateAt, - Message: posts[2].Message, - }, - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[3].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[3].CreateAt, - Message: posts[3].Message, - }, - { - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[4].Id, - UserEmail: users[0].Email, - UserType: "user", - CreateAt: posts[4].CreateAt, - Message: posts[4].Message, - }, - } - require.ElementsMatch(t, equalUpdateAts, []*actiance_export.PostExport{ - messages[0], messages[1], messages[2], messages[3], messages[4]}) - }) - - t.Run("GlobalRelay e2e 4 - test edits with multiple simultaneous updates", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret := generateE2ETestType4Results(t, th, model.ComplianceExportTypeGlobalrelayZip, attachmentDir, - exportDir, attachmentBackend, exportBackend) - jl := ret.joinLeaves - posts := ret.posts - batchTimes := ret.batchTimes - //jobStartTime := ret.start - batches := ret.batches - users := ret.users - channels := ret.channels - teams := ret.teams - - batchStartTime := batchTimes[0].start - batchEndTime := batchTimes[0].end - - // summaryHeader - allExpected := []string{fmt.Sprintf(grE2E4Summary, - // 1 2 3 4 - conv(batchStartTime), conv(batchEndTime), conv(jl[0].join), conv(batchEndTime), - // 5 6 7 8 9 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 10 11 12 13 - conv(posts[0].CreateAt), conv(posts[1].CreateAt), conv(posts[2].CreateAt), conv(posts[3].CreateAt), - // 14 15 16 17 18 19 - conv(posts[4].CreateAt), posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, - )} - - // summary body - allExpected = append(allExpected, - fmt.Sprintf("%[1]s %[2]s @user1 %[3]s @user1 user (user1@email) edited message 0 EditedNewMsg %[4]s", - posts[1].Id, conv(posts[0].CreateAt), users[0].Id, conv(posts[0].UpdateAt)), - fmt.Sprintf("* %[1]s %[2]s @user1 %[3]s @user1 user (user1@email) message 0 EditedOriginalMsg %[4]s %[5]s", - posts[0].Id, conv(posts[1].CreateAt), users[0].Id, conv(posts[0].UpdateAt), posts[1].Id), - fmt.Sprintf("* %[1]s %[2]s @user1 %[3]s @user1 user (user1@email) message 2", - posts[2].Id, conv(posts[2].CreateAt), users[0].Id), - fmt.Sprintf("* %[1]s %[2]s @user1 %[3]s @user1 user (user1@email) message 3", - posts[3].Id, conv(posts[3].CreateAt), users[0].Id), - fmt.Sprintf("* %[1]s %[2]s @user1 %[3]s @user1 user (user1@email) message 4", - posts[4].Id, conv(posts[4].CreateAt), users[0].Id), - ) - - // first two are channel and participants, rest are messages - allExpected = append(allExpected, - fmt.Sprintf("
    \n \n
    \n", - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, conv(batchStartTime), conv(batchEndTime)), - fmt.Sprintf("%[1]s\n @user1\n user\n user1@email\n %[2]s\n %[3]s\n 0 seconds\n 5", - users[0].Id, conv(jl[0].join), conv(batchEndTime)), - fmt.Sprintf("%[1]s\n %[2]s\n @user1\n %[3]s\n @user1\n user\n (user1@email)\n edited message 0\n EditedNewMsg\n %[4]s\n ", - posts[1].Id, conv(posts[1].CreateAt), users[0].Id, conv(posts[1].UpdateAt)), - fmt.Sprintf("%[1]s\n %[2]s\n @user1\n %[3]s\n @user1\n user\n (user1@email)\n message 0\n EditedOriginalMsg\n %[4]s\n %[5]s", - posts[0].Id, conv(posts[0].CreateAt), users[0].Id, conv(posts[0].UpdateAt), posts[1].Id), - fmt.Sprintf("%[1]s\n %[2]s\n @user1\n %[3]s\n @user1\n user\n (user1@email)\n message 2", - posts[2].Id, conv(posts[2].CreateAt), users[0].Id, conv(posts[2].UpdateAt)), - fmt.Sprintf("%[1]s\n %[2]s\n @user1\n %[3]s\n @user1\n user\n (user1@email)\n message 3", - posts[3].Id, conv(posts[3].CreateAt), users[0].Id, conv(posts[3].UpdateAt)), - fmt.Sprintf("%[1]s\n %[2]s\n @user1\n %[3]s\n @user1\n user\n (user1@email)\n message 4", - posts[4].Id, conv(posts[4].CreateAt), users[0].Id, conv(posts[4].UpdateAt)), - ) - - data := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - // clean some bad csrf if present - msg := global_relay_export.CleanTestOutput(data) - - for _, expected := range allExpected { - assert.Contains(t, msg, expected, "expected exported msg to contain: \n%s\n\nExported msg:\n%s\n", expected, msg) - if !strings.Contains(msg, expected) { - break - } - } - }) - - t.Run("CSV e2e 4 - test edits with multiple simultaneous updates", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - ret := generateE2ETestType4Results(t, th, model.ComplianceExportTypeCsv, attachmentDir, - exportDir, attachmentBackend, exportBackend) - jl := ret.joinLeaves - posts := ret.posts - batches := ret.batches - users := ret.users - channels := ret.channels - teams := ret.teams - - // fill out the lines using the template (easier to read than if it were an inline string) - tmpl := fmt.Sprintf(csvE2E4Batch1, - // 1 2 3 4 5 - teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, users[0].Id, - // 6 7 8 9 10 - posts[0].Id, posts[1].Id, posts[2].Id, posts[3].Id, posts[4].Id, - // 11 12 13 14 - jl[0].join, posts[0].CreateAt, posts[2].CreateAt, posts[1].UpdateAt) - - allExpected := strings.Split(tmpl, "\n") - - export := openZipAndReadFileNum(t, exportBackend, batches[0], 0) - - assert.Len(t, strings.Split(export, "\n"), len(allExpected)+1) // +1 for header line - - for _, expected := range allExpected { - assert.Contains(t, export, expected, "expected export to contain: \n%s\n\nExport:\n%s\n", expected, export) - if !strings.Contains(export, expected) { - break - } - } - }) - - t.Run("actiance e2e 5 - test delete and update semantics", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - - rets, ret5s := generateE2ETestType5Results(t, th, model.ComplianceExportTypeActiance, attachmentDir, exportDir, attachmentBackend, exportBackend) - posts := rets[0].posts - message0DeleteAt := ret5s[0].message0DeleteAt - zipBytes := ret5s[0].zipBytes[0] - - // - // Job 1 - // - xmlContents := readFileFromZip(t, zipBytes, "actiance_export.xml") - - exportedChannels := actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages := exportedChannels[0].Messages - require.Len(t, messages, 2) // 1 posted, 1 deleted - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: posts[0].Message, - }, messages[0]) - - // 1 - post deleted - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: "delete " + posts[0].Message, - UpdateAt: message0DeleteAt, - UpdatedType: shared.Deleted, - }, messages[1]) - - // - // Job 2 - // - posts = rets[1].posts - message0DeleteAt = ret5s[1].message0DeleteAt - zipBytes = ret5s[1].zipBytes[0] - zipBytes2 := ret5s[1].zipBytes[1] - - xmlContents = readFileFromZip(t, zipBytes, "actiance_export.xml") - - exportedChannels = actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages = exportedChannels[0].Messages - require.Len(t, messages, 1) // 1 posted - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: posts[1].Message, - }, messages[0]) - - xmlContents = readFileFromZip(t, zipBytes2, "actiance_export.xml") - - exportedChannels = actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages = exportedChannels[0].Messages - require.Len(t, messages, 2) // message0's create and delete messages - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: posts[0].Message, - }, messages[0]) - - // 1 - post deleted - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: "delete " + posts[0].Message, - UpdateAt: message0DeleteAt, - UpdatedType: shared.Deleted, - }, messages[1]) - - // - // Job 3 - // - posts = rets[2].posts - zipBytes = ret5s[2].zipBytes[0] - - xmlContents = readFileFromZip(t, zipBytes, "actiance_export.xml") - - exportedChannels = actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages = exportedChannels[0].Messages - require.Len(t, messages, 2) // 2 posted - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: posts[0].Message, - }, messages[0]) - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: posts[1].Message, - }, messages[1]) - - // - // Job 4 - // - posts = rets[3].posts - updatedPost1 := ret5s[3].updatedPost1 - message0DeleteAt = ret5s[3].message0DeleteAt - zipBytes = ret5s[3].zipBytes[0] - - xmlContents = readFileFromZip(t, zipBytes, "actiance_export.xml") - - exportedChannels = actiance_export.GetChannelExports(t, strings.NewReader(xmlContents)) - assert.Len(t, exportedChannels, 1) - messages = exportedChannels[0].Messages - require.Len(t, messages, 3) //filler post, deleted post, and updated posts ONLY - - // post created - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[2].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[2].CreateAt, - Message: posts[2].Message, - }, messages[0]) - - // post deleted ONLY (not its created post, because that was in the previous job) - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[0].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[0].CreateAt, - Message: "delete " + posts[0].Message, - UpdateAt: message0DeleteAt, - UpdatedType: shared.Deleted, - }, messages[1]) - - // post updated ONLY (not its created post, because that was in the previous job) - require.Equal(t, &actiance_export.PostExport{ - XMLName: xml.Name{Local: "Message"}, - MessageId: posts[1].Id, - UserEmail: th.BasicUser.Email, - UserType: "user", - CreateAt: posts[1].CreateAt, - Message: posts[1].Message, - UpdateAt: updatedPost1.UpdateAt, - UpdatedType: shared.UpdatedNoMsgChange, - }, messages[2]) - }) - - t.Run("GlobalRelay e2e 5 - test delete and update semantics", func(t *testing.T) { - // regMsg has strings in pos: 1: post_id, 2: sent_time, 3: username, 4: userId, 5: email, 6: message - regMsgTmpl := "
  • \n %[1]s\n %[2]s\n @%[3]s\n %[4]s\n @%[3]s\n user\n (%[5]s)\n %[6]s\n
  • " - // updatedMsgTmpl has strings in pos: 1: post_id, 2: sent_time, 3: username, 4: userId, 5: email, 6: message, 7: update_type, 8: update_time, 9: edited_new_msg_id - updatedMsgTmpl := "
  • \n %[1]s\n %[2]s\n @%[3]s\n %[4]s\n @%[3]s\n user\n (%[5]s)\n %[6]s\n %[7]s\n %[8]s\n %[9]s\n
  • \n" - - assertContainsAllMsgs := func(msg string, allExpected []string, tag string) { - for _, expected := range allExpected { - assert.Contains(t, msg, expected, "%s, expected exported msg to contain: \n%s\n\nExported msg:\n%s\n", tag, expected, msg) - if !strings.Contains(msg, expected) { - break - } - } - - numMessages := 0 - for l := range strings.SplitSeq(msg, "\n") { - if strings.Contains(l, "
  • ") { - numMessages += 1 - } - } - - assert.Equal(t, len(allExpected), numMessages, tag) - } - - th := setup(t) - defer th.TearDown() - - rets, ret5s := generateE2ETestType5Results(t, th, model.ComplianceExportTypeGlobalrelayZip, attachmentDir, exportDir, attachmentBackend, exportBackend) - posts := rets[0].posts - zipBytes := ret5s[0].zipBytes[0] - - // - // Job 1 - // - data := readFilenumFromZip(t, zipBytes, 0) - // clean some bad csrf if present - msg := global_relay_export.CleanTestOutput(data) - message0DeleteAt := ret5s[0].message0DeleteAt - - // post created - allExpected := []string{ - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[0].Message), - // 1 2 3 4 5 6 7 8 9 - fmt.Sprintf(updatedMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, "delete "+posts[0].Message, shared.Deleted, conv(message0DeleteAt), ""), - } - assertContainsAllMsgs(msg, allExpected, "job 1") - - // - // Job 2 - // - posts = rets[1].posts - message0DeleteAt = ret5s[1].message0DeleteAt - zipBytes = ret5s[1].zipBytes[0] - zipBytes2 := ret5s[1].zipBytes[1] - - data = readFilenumFromZip(t, zipBytes, 0) - // clean some bad csrf if present - msg = global_relay_export.CleanTestOutput(data) - - // post created - allExpected = []string{ - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[1].Id, conv(posts[1].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[1].Message), - } - assertContainsAllMsgs(msg, allExpected, "job 2a") - - data = readFilenumFromZip(t, zipBytes2, 0) - // clean some bad csrf if present - msg = global_relay_export.CleanTestOutput(data) - - allExpected = []string{ - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[0].Message), - // 1 2 3 4 5 6 7 8 9 - fmt.Sprintf(updatedMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, "delete "+posts[0].Message, shared.Deleted, conv(message0DeleteAt), ""), - } - assertContainsAllMsgs(msg, allExpected, "job 2b") - - // - // Job 3 - // - posts = rets[2].posts - zipBytes = ret5s[2].zipBytes[0] - - data = readFilenumFromZip(t, zipBytes, 0) - // clean some bad csrf if present - msg = global_relay_export.CleanTestOutput(data) - - // post created - allExpected = []string{ - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[0].Message), - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[1].Id, conv(posts[1].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[1].Message), - } - assertContainsAllMsgs(msg, allExpected, "job 3") - - // - // Job 4 - // - posts = rets[3].posts - updatedPost1 := ret5s[3].updatedPost1 - message0DeleteAt = ret5s[3].message0DeleteAt - zipBytes = ret5s[3].zipBytes[0] - - data = readFilenumFromZip(t, zipBytes, 0) - // clean some bad csrf if present - msg = global_relay_export.CleanTestOutput(data) - - allExpected = []string{ - // post created - // 1 2 3 4 5 6 - fmt.Sprintf(regMsgTmpl, posts[2].Id, conv(posts[2].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[2].Message), - - // post deleted ONLY (not its created post, because that was in the previous job) - // 1 2 3 4 5 6 7 8 9 - fmt.Sprintf(updatedMsgTmpl, posts[0].Id, conv(posts[0].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, "delete "+posts[0].Message, shared.Deleted, conv(message0DeleteAt), ""), - - // post updated ONLY (not its created post, because that was in the previous job) - // 1 2 3 4 5 6 7 8 9 - fmt.Sprintf(updatedMsgTmpl, posts[1].Id, conv(posts[1].CreateAt), th.BasicUser.Username, th.BasicUser.Id, th.BasicUser.Email, posts[1].Message, shared.UpdatedNoMsgChange, conv(updatedPost1.UpdateAt), ""), - } - assertContainsAllMsgs(msg, allExpected, "job 4") - }) - - t.Run("CSV e2e 5 - test delete and update semantics", func(t *testing.T) { - type msgDetailsToCheck struct { - createAt int64 - updateAt int64 - updatedType shared.PostUpdatedType - postId string - message string - } - - assertContainsAllMsgs := func(msg string, allExpected []msgDetailsToCheck, tag string) { - allLines := strings.Split(strings.Trim(msg, " \n"), "\n") - allLines = allLines[1:] // remove header - var msgLines []string - for _, l := range allLines { - if !strings.Contains(l, "previously-joined") { - msgLines = append(msgLines, l) - } - } - - assert.Equal(t, len(allExpected), len(msgLines), tag) - - for _, expected := range allExpected { - found := false - for _, msg := range msgLines { - if strings.HasPrefix(msg, fmt.Sprintf("%d,%d,%s,", - expected.createAt, expected.updateAt, expected.updatedType)) && - strings.Contains(msg, expected.postId+",,,"+expected.message) { - found = true - break - } - } - assert.True(t, found, "%s actual msg did not contain expected msg. msg: \n%s\nexpected msg details: %v\n", tag, msg, expected) - } - } - - th := setup(t) - defer th.TearDown() - - rets, ret5s := generateE2ETestType5Results(t, th, model.ComplianceExportTypeCsv, attachmentDir, exportDir, attachmentBackend, exportBackend) - posts := rets[0].posts - message0DeleteAt := ret5s[0].message0DeleteAt - zipBytes := ret5s[0].zipBytes[0] - - // NOTE: we know csv outputs correctly from above, so just test that the right post IDs are being exported - - // - // Job 1 - // - export := readFilenumFromZip(t, zipBytes, 0) - - // post created, post deleted - assertContainsAllMsgs(export, []msgDetailsToCheck{ - { - createAt: posts[0].CreateAt, - updateAt: message0DeleteAt, - postId: posts[0].Id, - message: posts[0].Message, - }, - { - createAt: posts[0].CreateAt, - updateAt: message0DeleteAt, - updatedType: shared.Deleted, - postId: posts[0].Id, - message: "delete " + posts[0].Message, - }, - }, "Job 1") - - // - // Job 2 - // - posts = rets[1].posts - message0DeleteAt = ret5s[1].message0DeleteAt - zipBytes = ret5s[1].zipBytes[0] - zipBytes2 := ret5s[1].zipBytes[1] - - export = readFilenumFromZip(t, zipBytes, 0) - - // post created - assertContainsAllMsgs(export, []msgDetailsToCheck{ - { - createAt: posts[1].CreateAt, - updateAt: posts[1].UpdateAt, - postId: posts[1].Id, - message: posts[1].Message, - }, - }, "Job 2 batch 1") - - export = readFilenumFromZip(t, zipBytes2, 0) - - // post created - assertContainsAllMsgs(export, []msgDetailsToCheck{ - { - createAt: posts[0].CreateAt, - updateAt: message0DeleteAt, - postId: posts[0].Id, - message: posts[0].Message, - }, - { - createAt: posts[0].CreateAt, - updateAt: message0DeleteAt, - updatedType: shared.Deleted, - postId: posts[0].Id, - message: "delete " + posts[0].Message, - }, - }, "Job 2 batch 2") - - // Job 3 - // - posts = rets[2].posts - zipBytes = ret5s[2].zipBytes[0] - - export = readFilenumFromZip(t, zipBytes, 0) - - // 2 posts created - assertContainsAllMsgs(export, []msgDetailsToCheck{ - { - createAt: posts[0].CreateAt, - updateAt: posts[0].UpdateAt, - postId: posts[0].Id, - message: posts[0].Message, - }, - { - createAt: posts[1].CreateAt, - updateAt: posts[1].UpdateAt, - postId: posts[1].Id, - message: posts[1].Message, - }, - }, "Job 3") - - // - // Job 4 - // - posts = rets[3].posts - updatedPost1 := ret5s[3].updatedPost1 - message0DeleteAt = ret5s[3].message0DeleteAt - zipBytes = ret5s[3].zipBytes[0] - - export = readFilenumFromZip(t, zipBytes, 0) - - // post created - assertContainsAllMsgs(export, []msgDetailsToCheck{ - { - createAt: posts[2].CreateAt, - updateAt: posts[2].UpdateAt, - postId: posts[2].Id, - message: posts[2].Message, - }, - // post deleted ONLY (not its created post, because that was in the previous job) - { - createAt: posts[0].CreateAt, - updateAt: message0DeleteAt, - updatedType: shared.Deleted, - postId: posts[0].Id, - message: "delete " + posts[0].Message, - }, - // post updated ONLY (not its created post, because that was in the previous job) - { - createAt: posts[1].CreateAt, - updateAt: updatedPost1.UpdateAt, - updatedType: shared.UpdatedNoMsgChange, - postId: posts[1].Id, - message: posts[1].Message, - }, - }, "Job 4") - }) - - t.Run("csv -- multiple batches, 1 zip per batch, output to a single directory", func(t *testing.T) { - th := setup(t) - defer th.TearDown() - defer func() { - err := os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - }() - - time.Sleep(1 * time.Millisecond) - now := model.GetMillis() - - jobStart := now - 1 - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = jobStart - *cfg.MessageExportSettings.BatchSize = 5 - *cfg.MessageExportSettings.ExportFormat = model.ComplianceExportTypeCsv - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - attachmentContent := "Hello there" - attachmentPath001 := "path/to/attachments/one.txt" - _, _ = attachmentBackend.WriteFile(bytes.NewBufferString(attachmentContent), attachmentPath001) - post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now, - UpdateAt: now, - FileIds: []string{"test1"}, - }) - require.NoError(t, err) - - attachment, err := th.App.Srv().Store().FileInfo().Save(th.Context, &model.FileInfo{ - Id: st.NewTestID(), - CreatorId: post.UserId, - PostId: post.Id, - CreateAt: now, - UpdateAt: now, - Path: attachmentPath001, - }) - require.NoError(t, err) - - for i := range 10 { - _, e := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now + int64(i), - UpdateAt: now + int64(i), - }) - require.NoError(t, e) - } - - job := runJobForTest(t, th, nil) - - warnings, err := strconv.Atoi(job.Data[shared.JobDataWarningCount]) - require.NoError(t, err) - require.Equal(t, 0, warnings) - - numExported, err := strconv.ParseInt(job.Data[shared.JobDataMessagesExported], 0, 64) - require.NoError(t, err) - require.Equal(t, int64(11), numExported) - jobEnd, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 0, 64) - require.NoError(t, err) - - jobExportDir := job.Data[shared.JobDataExportDir] - batch001 := shared.GetBatchPath(jobExportDir, jobStart, now+3, 1) - batch002 := shared.GetBatchPath(jobExportDir, now+3, now+8, 2) - batch003 := shared.GetBatchPath(jobExportDir, now+8, jobEnd, 3) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - require.ElementsMatch(t, []string{batch001, batch002, batch003}, files) - - zipBytes, err := exportBackend.ReadFile(batch001) - require.NoError(t, err) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - csvZipFilePath := path.Join("files", post.Id, fmt.Sprintf("%s-%s", attachment.Id, path.Base(attachment.Path))) - - attachmentInZip, err := zipReader.Open(csvZipFilePath) - require.NoError(t, err) - attachmentInZipContents, err := io.ReadAll(attachmentInZip) - require.NoError(t, err) - err = attachmentInZip.Close() - require.NoError(t, err) - - require.EqualValuesf(t, attachmentContent, string(attachmentInZipContents), "file contents not equal") - }) -} - -func openZipAndReadFile(t *testing.T, backend filestore.FileBackend, path string, filename string) string { - zipBytes, err := backend.ReadFile(path) - require.NoError(t, err) - return readFileFromZip(t, zipBytes, filename) -} - -func readFileFromZip(t *testing.T, zipBytes []byte, filename string) string { - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - file, err := zipReader.Open(filename) - require.NoError(t, err) - contents, err := io.ReadAll(file) - require.NoError(t, err) - err = file.Close() - require.NoError(t, err) - - return string(contents) -} - -func openZipAndReadFileNum(t *testing.T, backend filestore.FileBackend, path string, fileNum int) string { - zipBytes, err := backend.ReadFile(path) - require.NoError(t, err) - return readFilenumFromZip(t, zipBytes, fileNum) -} - -func openZipAndReadFileStartingWith(t *testing.T, backend filestore.FileBackend, path string, startsWith string) string { - zipBytes, err := backend.ReadFile(path) - require.NoError(t, err) - - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - var names []string - for _, f := range zipReader.File { - if strings.HasPrefix(f.Name, startsWith) { - file, err := f.Open() - require.NoError(t, err) - contents, err := io.ReadAll(file) - require.NoError(t, err) - err = file.Close() - require.NoError(t, err) - - return string(contents) - } - names = append(names, f.Name) - } - - require.True(t, false, "called openZipAndReadFileStartingWith but didn't file file starting with: %s. Found: %v", startsWith, names) - return "" -} - -func readFilenumFromZip(t *testing.T, zipBytes []byte, fileNum int) string { - zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) - require.NoError(t, err) - - file, err := zipReader.File[fileNum].Open() - require.NoError(t, err) - contents, err := io.ReadAll(file) - require.NoError(t, err) - err = file.Close() - require.NoError(t, err) - - return string(contents) -} - -func dataContainsOneOfExpected(data string, expected []string) bool { - for _, perm := range expected { - if strings.Contains(data, perm) { - return true - } - } - return false -} diff --git a/server/enterprise/message_export/message_export_test_e2e_generators.go b/server/enterprise/message_export/message_export_test_e2e_generators.go deleted file mode 100644 index c9781bd27c9..00000000000 --- a/server/enterprise/message_export/message_export_test_e2e_generators.go +++ /dev/null @@ -1,1551 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "bytes" - "context" - "fmt" - "os" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/api4" - "github.com/mattermost/mattermost/server/v8/channels/jobs" - st "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -type MessageExport struct { - TeamId string - TeamName string - TeamDisplayName string - - ChannelId string - ChannelName string - ChannelDisplayName string - ChannelType model.ChannelType - - UserId string - UserEmail string - Username string - IsBot bool - - PostId string - PostCreateAt int64 - PostUpdateAt int64 - PostDeleteAt int64 - PostEditAt int64 - PostMessage string - PostType string - PostRootId string - PostProps string - PostOriginalId string - PostFileIds []string -} - -func removePointers(p *model.MessageExport) MessageExport { - return MessageExport{ - TeamId: model.SafeDereference(p.TeamId), - TeamName: model.SafeDereference(p.TeamName), - TeamDisplayName: model.SafeDereference(p.TeamDisplayName), - ChannelId: model.SafeDereference(p.ChannelId), - ChannelName: model.SafeDereference(p.ChannelName), - ChannelDisplayName: model.SafeDereference(p.ChannelDisplayName), - ChannelType: model.SafeDereference(p.ChannelType), - UserId: model.SafeDereference(p.UserId), - UserEmail: model.SafeDereference(p.UserEmail), - Username: model.SafeDereference(p.Username), - IsBot: p.IsBot, - PostId: model.SafeDereference(p.PostId), - PostCreateAt: model.SafeDereference(p.PostCreateAt), - PostUpdateAt: model.SafeDereference(p.PostUpdateAt), - PostDeleteAt: model.SafeDereference(p.PostDeleteAt), - PostEditAt: model.SafeDereference(p.PostEditAt), - PostMessage: model.SafeDereference(p.PostMessage), - PostType: model.SafeDereference(p.PostType), - PostRootId: model.SafeDereference(p.PostRootId), - PostProps: model.SafeDereference(p.PostProps), - PostOriginalId: model.SafeDereference(p.PostOriginalId), - PostFileIds: p.PostFileIds, - } -} - -// assertNumPostsToExport checks both the MessageExport and the AnalyticsPostCount -- they were sometimes giving -// different numbers and this helps debug. -func assertNumPostsToExport(t *testing.T, th *api4.TestHelper, num int, since, until int64) { - exports, _, err := th.App.Srv().Store().Compliance().MessageExport(th.Context, model.MessageExportCursor{ - LastPostUpdateAt: since, UntilUpdateAt: until, - }, 100) - assert.NoError(t, err) - assert.Len(t, exports, num) - assert.Lenf(t, exports, num, "MessageExport posts found, since %d, th.BasicChannel.Id: %s\n", since, th.BasicChannel.Id) - if len(exports) != num { - for _, p := range exports { - t.Logf("%#+v\n", removePointers(p)) - } - } - - count, err := th.App.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeSystemPosts: true, UsersPostsOnly: true, SincePostID: "", SinceUpdateAt: since}) - require.NoError(t, err) - assert.Equal(t, num, int(count)) -} - -func assertNumExported(t *testing.T, expectedNum int, data map[string]string) { - numExported, err := strconv.Atoi(data[shared.JobDataMessagesExported]) - require.NoError(t, err) - assert.Equalf(t, numExported, expectedNum, "\njobData: %v\n", data) - require.Equal(t, expectedNum, numExported) -} - -func getMostRecentJobWithId(t *testing.T, th *api4.TestHelper, id string) *model.Job { - list, _, err := th.SystemAdminClient.GetJobsByType(context.Background(), "message_export", 0, 1) - require.NoError(t, err) - require.Len(t, list, 1) - require.Equal(t, id, list[0].Id) - return list[0] -} - -func checkJobForStatus(t *testing.T, th *api4.TestHelper, id string, status string) { - doneChan := make(chan bool) - var job *model.Job - go func() { - defer close(doneChan) - for { - job = getMostRecentJobWithId(t, th, id) - if job.Status == status { - break - } - time.Sleep(100 * time.Millisecond) - } - require.Equal(t, status, job.Status) - }() - select { - case <-doneChan: - case <-time.After(15 * time.Second): - require.True(t, false, "expected job's status to be %s, got %s", status, job.Status) - } -} - -func runJobForTest(t *testing.T, th *api4.TestHelper, jobData map[string]string) *model.Job { - job, _, err := th.SystemAdminClient.CreateJob(context.Background(), - &model.Job{Type: "message_export", Data: jobData}) - require.NoError(t, err) - // poll until completion - checkJobForStatus(t, th, job.Id, "success") - job = getMostRecentJobWithId(t, th, job.Id) - return job -} - -func setup(t *testing.T) *api4.TestHelper { - jobs.DefaultWatcherPollingInterval = 100 - th := api4.SetupEnterprise(t).InitBasic() - th.App.Srv().SetLicense(model.NewTestLicense("message_export")) - messageExportImpl := MessageExportJobInterfaceImpl{th.App.Srv()} - th.App.Srv().Jobs.RegisterJobType(model.JobTypeMessageExport, messageExportImpl.MakeWorker(), messageExportImpl.MakeScheduler()) - - err := th.App.Srv().Jobs.StartWorkers() - require.NoError(t, err) - - err = th.App.Srv().Jobs.StartSchedulers() - require.NoError(t, err) - - return th -} - -// jobDataInvariantsShouldBeEqual tests that the parts of the job.Data that shouldn't change, don't change. -func jobDataInvariantsShouldBeEqual(t *testing.T, expected map[string]string, received map[string]string) { - assert.Equal(t, expected[shared.JobDataExportType], received[shared.JobDataExportType]) - assert.Equal(t, expected[shared.JobDataBatchSize], received[shared.JobDataBatchSize]) - assert.Equal(t, expected[shared.JobDataChannelBatchSize], received[shared.JobDataChannelBatchSize]) - assert.Equal(t, expected[shared.JobDataChannelHistoryBatchSize], received[shared.JobDataChannelHistoryBatchSize]) - assert.Equal(t, expected[shared.JobDataExportDir], received[shared.JobDataExportDir]) - assert.Equal(t, expected[shared.JobDataJobEndTime], received[shared.JobDataJobEndTime]) - assert.Equal(t, expected[shared.JobDataJobStartTime], received[shared.JobDataJobStartTime]) -} - -type joinLeave struct { - join int64 - leave int64 -} -type batchStartEndTimes struct { - start int64 - end int64 -} - -type JobResults struct { - start int64 - joinLeaves []joinLeave - users []*model.User - createUpdateTimes []int64 - attachments []*model.FileInfo - contents []string - jobEndTime int64 - batches []string - posts []*model.Post - channels []*model.Channel - teams []*model.Team - batchTimes []batchStartEndTimes - jobExportDir string -} - -func generateActianceBatchTest1(t *testing.T, th *api4.TestHelper, attachmentDir, exportDir string, - attachmentBackend filestore.FileBackend) JobResults { - now := model.GetMillis() - jobStart := now - 1 - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = jobStart - *cfg.MessageExportSettings.BatchSize = 5 - *cfg.MessageExportSettings.ExportFormat = model.ComplianceExportTypeActiance - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - contents := []string{"Hello there"} - attachmentPath001 := "path/to/attachments/one.txt" - _, _ = attachmentBackend.WriteFile(bytes.NewBufferString(contents[0]), attachmentPath001) - post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now, - UpdateAt: now, - FileIds: []string{"test1"}, - }) - require.NoError(t, err) - - attachment, err := th.App.Srv().Store().FileInfo().Save(th.Context, &model.FileInfo{ - Id: st.NewTestID(), - CreatorId: post.UserId, - PostId: post.Id, - CreateAt: now, - UpdateAt: now, - Path: attachmentPath001, - }) - require.NoError(t, err) - attachments := []*model.FileInfo{attachment} - - for i := range 10 { - _, e := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now + int64(i), - UpdateAt: now + int64(i), - }) - require.NoError(t, e) - } - - until := model.GetMillis() - assertNumPostsToExport(t, th, 11, jobStart, until) - - job := runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - warnings, err := strconv.Atoi(job.Data[shared.JobDataWarningCount]) - require.NoError(t, err) - require.Equal(t, 0, warnings) - - numExported, err := strconv.ParseInt(job.Data[shared.JobDataMessagesExported], 0, 64) - require.NoError(t, err) - require.Equal(t, int64(11), numExported) - - jobEnd, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 0, 64) - require.NoError(t, err) - jobExportDir := job.Data[shared.JobDataExportDir] - batch001 := shared.GetBatchPath(jobExportDir, jobStart, now+3, 1) - batch002 := shared.GetBatchPath(jobExportDir, now+3, now+8, 2) - batch003 := shared.GetBatchPath(jobExportDir, now+8, jobEnd, 3) - batches := []string{batch001, batch002, batch003} - - return JobResults{ - attachments: attachments, - contents: contents, - batches: batches, - jobExportDir: jobExportDir, - } -} - -func generateActianceBatchTest2(t *testing.T, th *api4.TestHelper, attachmentDir, exportDir string) JobResults { - now := model.GetMillis() - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.BatchSize = 3 - *cfg.MessageExportSettings.ExportFormat = model.ComplianceExportTypeActiance - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - for i := range 10 { - _, e := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: th.BasicChannel.Id, - UserId: st.NewTestID(), - Message: st.NewTestID(), - CreateAt: now + int64(i), - UpdateAt: now + int64(i), - }) - require.NoError(t, e) - } - - // start at the 2nd post and get till the 7th post (inclusive) = 6 posts - job := runJobForTest(t, th, map[string]string{ - shared.JobDataBatchStartTime: strconv.Itoa(int(now) + 1), - shared.JobDataJobEndTime: strconv.Itoa(int(now) + 6), - }) - numExported, err := strconv.ParseInt(job.Data[shared.JobDataMessagesExported], 0, 64) - require.NoError(t, err) - numExpected, err := strconv.ParseInt(job.Data[shared.JobDataTotalPostsExpected], 0, 64) - require.NoError(t, err) - // test that we only exported 6 (because the JobDataJobEndTime was translated to the cursor's UntilUpdateAt) - require.Equal(t, 6, int(numExported)) - // test that we were reporting that correctly in the UI - require.Equal(t, 6, int(numExpected)) - - jobEnd, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 0, 64) - require.NoError(t, err) - require.Equal(t, now+6, jobEnd) - jobExportDir := job.Data[shared.JobDataExportDir] - batch001 := shared.GetBatchPath(jobExportDir, now+1, now+3, 1) - // lastPostUpdateAt will be post#4 (now+3), even though we exported it above, because LastPostId will exclude it - batch002 := shared.GetBatchPath(jobExportDir, now+3, now+6, 2) - batches := []string{batch001, batch002} - - return JobResults{ - batches: batches, - jobExportDir: jobExportDir, - } -} - -func generateE2ETestType1Results(t *testing.T, th *api4.TestHelper, exportType, attachmentDir, exportDir string, - attachmentBackend, exportBackend filestore.FileBackend, testStopping bool) JobResults { - // This tests (reading the files exported and testing the actual exported data): - // - job system exports the complete time from beginning to end; i.e., it doesn't use the post updateAt values as the bounds, it uses the start time and end time of the job. - // - job system uses previous job's end time as the start for the next batch - // - user joins and leaves before the first post in the first batch (but after the job start time) - // - user joins and leaves before the first post in the second batch - // - user joins and leaves before the first post in the last batch - // - user joins and leaves after the last post in the last batch (but before the job end time) - // - channel with no posts but user activity (one user joins and leaves after start of batch period but before first post) - // - channel with no posts but user activity (one user leaves after last post but before end of batch period) - // - worked, but making sure we test for it specifically in e2e: - // - exports with multiple channels (this wasn't tested before) - // - user joins before job start time and stays (should record user's original join time, not the start of the job) - // - attachments are recorded with correct names in the xml and content in the files - // - a post from a user who wasn't a member in the channel creates a record for the user entering the channel at the start of the batch and leaving at the end of the batch (to be discussed with end user, this seems wrong) - - // Also tests the `BatchSize+1` logic in the worker, because we have 9 posts and batch size of 3. - - start := model.GetMillis() - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = 0 - *cfg.MessageExportSettings.BatchSize = 3 - *cfg.MessageExportSettings.ExportFormat = exportType - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - // Users: - users := make([]*model.User, 0) - user, err := th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user1", - Email: "user1@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user2", - Email: "user2@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user3", - Email: "user3@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user4", - Email: "user4@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user5", - Email: "user5@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user6", - Email: "user6@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user7", - Email: "user7@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user8", - Email: "user8@email", - }) - require.NoError(t, err) - users = append(users, user) - - channel2, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Two", - Name: "channel_two_name", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // channel3 will have only one user leaving during export time - channel3, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Three", - Name: "channel_three_name", - Type: model.ChannelTypeOpen, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // channel4 will have only one user joining during export time - channel4, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Four", - Name: "channel_four_name", - Type: model.ChannelTypeOpen, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // Save 9 posts so that we have the following batches: - createUpdateTimes := []int64{ - start + 10, start + 14, start + 20, // batch 1: start-20, posts start at 10 - start + 23, start + 25, start + 30, // batch 2: 20-30, posts start at 23 - start + 36, start + 37, start + 40, // batch 3: 30-40, posts start at 36 - } - - jl := []joinLeave{ - {start - 5, 0}, // user 1 never leaves - {start + 7, start + 8}, // user 2 joins and leaves before first post (but after startTime) - {start + 11, start + 15}, // user 3 joins and leaves during first batch - {start + 21, start + 22}, // user 4 joins and leaves during second batch but before second batch's post - {start + 32, start + 35}, // user 5 joins and leaves during third batch but before third batch's post - {start + 55, start + 57}, // user 6 joins and leaves after the last batch's last post but before when export is run - {start - 100, start + 5}, // user 7 joins channel3 before start time and leaves before batch 1 - {start + 59, 0}, // user 8 joins channel4 after batch 3 (but before end) - } - - // user 1 joins before start time and stays (and posts) - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[0].Id, channel2.Id, jl[0].join) - require.NoError(t, err) - // user 2 joins and leaves before first post (but after startTime) - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[1].Id, channel2.Id, jl[1].join) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[1].Id, channel2.Id, jl[1].leave) - require.NoError(t, err) - // user 3 joins and leaves during first batch - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[2].Id, channel2.Id, jl[2].join) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[2].Id, channel2.Id, jl[2].leave) - require.NoError(t, err) - // user 4 joins and leaves during second batch but before second batch's post - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[3].Id, channel2.Id, jl[3].join) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[3].Id, channel2.Id, jl[3].leave) - // user 5 joins and leaves during third batch but before third batch's post - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[4].Id, channel2.Id, jl[4].join) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[4].Id, channel2.Id, jl[4].leave) - require.NoError(t, err) - // user 6 joins and leaves after the last batch's last post - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[5].Id, channel2.Id, jl[5].join) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[5].Id, channel2.Id, jl[5].leave) - require.NoError(t, err) - - // user 7 joins channel3 before start time and leaves before batch 1 - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[6].Id, channel3.Id, start-100) - require.NoError(t, err) - err = th.App.Srv().Store().ChannelMemberHistory().LogLeaveEvent(users[6].Id, channel3.Id, start+5) - require.NoError(t, err) - // user 8 joins channel4 after batch 3 (but before end) - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[7].Id, channel4.Id, start+59) - require.NoError(t, err) - - assertNumPostsToExport(t, th, 0, start, model.GetMillis()) - - var attachments []*model.FileInfo - var contents []string - var posts []*model.Post - for i, updateAt := range createUpdateTimes { - post, err2 := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: fmt.Sprintf("message %d", i), - CreateAt: updateAt, - UpdateAt: updateAt, - FileIds: []string{fmt.Sprintf("test%d", i)}, - }) - require.NoError(t, err2) - posts = append(posts, post) - time.Sleep(time.Millisecond) - - attachmentContent := fmt.Sprintf("Hello there %d", i) - attachmentPath := fmt.Sprintf("path/to/attachments/file_%d.txt", i) - _, err = attachmentBackend.WriteFile(bytes.NewBufferString(attachmentContent), attachmentPath) - require.NoError(t, err) - - info, err2 := th.App.Srv().Store().FileInfo().Save(th.Context, &model.FileInfo{ - Id: st.NewTestID(), - CreatorId: post.UserId, - PostId: post.Id, - CreateAt: updateAt, - UpdateAt: updateAt, - Path: attachmentPath, - }) - require.NoError(t, err2) - attachments = append(attachments, info) - contents = append(contents, attachmentContent) - } - - // Test that it's picking up a previous successful job - var previousJob *model.Job - previousJob, err = th.App.Srv().Store().Job().GetNewestJobByStatusesAndType([]string{model.JobStatusWarning, model.JobStatusSuccess}, model.JobTypeMessageExport) - require.Error(t, err) - require.Nil(t, previousJob) - - _, err = th.App.Srv().Store().Job().Save(&model.Job{ - Id: "blah", - Type: model.JobTypeMessageExport, - Priority: 0, - CreateAt: 0, - StartAt: 0, - LastActivityAt: 0, - Status: model.JobStatusSuccess, - Progress: 100, - Data: map[string]string{shared.JobDataBatchStartTime: strconv.Itoa(int(start))}, - }) - require.NoError(t, err) - - previousJob, err = th.App.Srv().Store().Job().GetNewestJobByStatusesAndType([]string{model.JobStatusWarning, model.JobStatusSuccess}, model.JobTypeMessageExport) - require.NoError(t, err) - require.NotNilf(t, previousJob, "prevJob") - - var prevUpdatedAt int64 - if timestamp, prevExists := previousJob.Data[shared.JobDataBatchStartTime]; prevExists { - prevUpdatedAt, err = strconv.ParseInt(timestamp, 10, 64) - require.NoError(t, err) - } - require.Equal(t, prevUpdatedAt, start) - - // move past the last post time - time.Sleep(100 * time.Millisecond) - - // check number of messages to be exported - until := model.GetMillis() - assertNumPostsToExport(t, th, 9, start, until) - - // Now run the exports - var job *model.Job - if testStopping { - var jobData map[string]string - // manually create the job (which will start right away, so we need to wait for it below, after we use its id. - job, _, err = th.SystemAdminClient.CreateJob(context.Background(), &model.Job{ - Type: "message_export", - Data: map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}}) - require.NoError(t, err) - - // Stop the export after the second batch by stopping the Worker. - batchCount := 0 - testEndOfBatchCb = func(worker *MessageExportWorker) { - batchCount++ - if batchCount == 2 { - job = getMostRecentJobWithId(t, th, job.Id) - jobData = job.Data - - // let the job continue, but stop Worker, check we went back to Pending, then start the Worker. - go func() { - worker.Stop() - checkJobForStatus(t, th, job.Id, model.JobStatusPending) - worker.Run() - checkJobForStatus(t, th, job.Id, model.JobStatusInProgress) - }() - } - } - - // Wait for the rest of the exports to finish - checkJobForStatus(t, th, job.Id, model.JobStatusSuccess) - job = getMostRecentJobWithId(t, th, job.Id) - testEndOfBatchCb = nil - jobDataInvariantsShouldBeEqual(t, jobData, job.Data) - } else { - job = runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - } - - warnings, err := strconv.Atoi(job.Data[shared.JobDataWarningCount]) - require.NoError(t, err) - require.Equal(t, 0, warnings) - - assertNumExported(t, 9, job.Data) - - jobExportDir := job.Data[shared.JobDataExportDir] - jobEndTime, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batchTimes := []batchStartEndTimes{ - {start: prevUpdatedAt, end: createUpdateTimes[2]}, - {start: createUpdateTimes[2], end: createUpdateTimes[5]}, - {start: createUpdateTimes[5], end: jobEndTime}, - } - - batch001 := shared.GetBatchPath(jobExportDir, batchTimes[0].start, batchTimes[0].end, 1) - batch002 := shared.GetBatchPath(jobExportDir, batchTimes[1].start, batchTimes[1].end, 2) - batch003 := shared.GetBatchPath(jobExportDir, batchTimes[2].start, batchTimes[2].end, 3) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches := []string{batch001, batch002, batch003} - require.ElementsMatch(t, batches, files) - - return JobResults{ - start: start, - joinLeaves: jl, - users: users, - createUpdateTimes: createUpdateTimes, - attachments: attachments, - contents: contents, - jobEndTime: jobEndTime, - batches: batches, - posts: posts, - channels: []*model.Channel{channel2, channel3, channel4}, - teams: []*model.Team{th.BasicTeam}, - batchTimes: batchTimes, - } -} - -func generateE2ETestType2Results(t *testing.T, th *api4.TestHelper, exportType, attachmentDir, exportDir string, - attachmentBackend, exportBackend filestore.FileBackend) JobResults { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = 0 - *cfg.MessageExportSettings.BatchSize = 3 - *cfg.MessageExportSettings.ExportFormat = exportType - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - start := model.GetMillis() - - // Users: - users := make([]*model.User, 0) - user, err := th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user1", - Email: "user1@email", - }) - require.NoError(t, err) - users = append(users, user) - user, err = th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user2", - Email: "user2@email", - }) - require.NoError(t, err) - users = append(users, user) - - channel2, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Two", - Name: "channel_two_name", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - createUpdateTimes := []int64{start + 10, start + 14} - - jl := []joinLeave{ - {start - 5, 0}, // user 1 never leaves - } - - // user 1 joins before start time and stays (and posts) - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[0].Id, channel2.Id, jl[0].join) - require.NoError(t, err) - - assertNumPostsToExport(t, th, 0, start, model.GetMillis()) - - var posts []*model.Post - - // first post from user 1 (member) - post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 1", - CreateAt: createUpdateTimes[0], - UpdateAt: createUpdateTimes[0], - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[1].Id, - Message: "message 2", - CreateAt: createUpdateTimes[1], - UpdateAt: createUpdateTimes[1], - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // Test that it's picking up a previous successful job - var previousJob *model.Job - previousJob, err = th.App.Srv().Store().Job().GetNewestJobByStatusesAndType([]string{model.JobStatusWarning, model.JobStatusSuccess}, model.JobTypeMessageExport) - require.Error(t, err) - require.Nil(t, previousJob) - - _, err = th.App.Srv().Store().Job().Save(&model.Job{ - Id: "blah", - Type: model.JobTypeMessageExport, - Priority: 0, - CreateAt: 0, - StartAt: 0, - LastActivityAt: 0, - Status: model.JobStatusSuccess, - Progress: 100, - Data: map[string]string{shared.JobDataBatchStartTime: strconv.Itoa(int(start))}, - }) - require.NoError(t, err) - - previousJob, err = th.App.Srv().Store().Job().GetNewestJobByStatusesAndType([]string{model.JobStatusWarning, model.JobStatusSuccess}, model.JobTypeMessageExport) - require.NoError(t, err) - require.NotNilf(t, previousJob, "prevJob") - - var prevUpdatedAt int64 - if timestamp, prevExists := previousJob.Data[shared.JobDataBatchStartTime]; prevExists { - prevUpdatedAt, err = strconv.ParseInt(timestamp, 10, 64) - require.NoError(t, err) - } - require.Equal(t, prevUpdatedAt, start) - - // move past the last post time - time.Sleep(30 * time.Millisecond) - - // check number of messages to be exported - until := model.GetMillis() - assertNumPostsToExport(t, th, 2, start, until) - - // Now run the exports - job := runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - warnings, err := strconv.Atoi(job.Data[shared.JobDataWarningCount]) - require.NoError(t, err) - require.Equal(t, 0, warnings) - - assertNumExported(t, 2, job.Data) - - jobExportDir := job.Data[shared.JobDataExportDir] - jobEndTime, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batch001 := shared.GetBatchPath(jobExportDir, prevUpdatedAt, jobEndTime, 1) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches := []string{batch001} - require.ElementsMatch(t, batches, files) - - return JobResults{ - start: start, - joinLeaves: jl, - users: users, - createUpdateTimes: createUpdateTimes, - jobEndTime: jobEndTime, - batches: batches, - posts: posts, - channels: []*model.Channel{channel2}, - teams: []*model.Team{th.BasicTeam}, - batchTimes: []batchStartEndTimes{{start, jobEndTime}}, - } -} - -// Type3Results specific data needed to be returned by this test only -type Type3Results struct { - message1DeleteAt int64 - updatedPost2 *model.Post - message3AndFileInfoDeleteAt int64 - deletedPost3 *model.Post -} - -func generateE2ETestType3Results(t *testing.T, th *api4.TestHelper, exportType, attachmentDir, exportDir string, - attachmentBackend, exportBackend filestore.FileBackend) (JobResults, Type3Results) { - // This tests (reading the files exported and testing the exported xml): - // - post create at field is set - // - post deleted fields are set - // - post updated (not edited) - // - post deleted with a deleted file - // - post edited (new message created with original message, old message updated) - // - post edited with 3 simultaneous posts in-between - forward - // - post edited but falls on the batch boundary (originalId is in batch 1, newId is batch 2) - - start := model.GetMillis() - - // Users: - users := make([]*model.User, 0) - user, err := th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user1", - Email: "user1@email", - }) - require.NoError(t, err) - users = append(users, user) - - // only testing one channel - channel2, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Two", - Name: "channel_two_name", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // user 1 joins before start time and stays (and posts) - user1JoinTime := start - 100 - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[0].Id, channel2.Id, user1JoinTime) - require.NoError(t, err) - - jl := []joinLeave{ - {user1JoinTime, 0}, // user 1 never leaves - } - - assertNumPostsToExport(t, th, 0, start, model.GetMillis()) - - var attachments []*model.FileInfo - var contents []string - var posts []*model.Post - - // 0 - post create - post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 0", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 1 - post deleted - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 1", - }) - require.NoError(t, err) - message1DeleteAt := model.GetMillis() - err = th.App.Srv().Store().Post().Delete(th.Context, post.Id, message1DeleteAt, users[0].Id) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 2 - post updated not edited (e.g., reaction) - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 2", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - _, err = th.App.Srv().Store().Reaction().Save(&model.Reaction{ - UserId: users[0].Id, - PostId: post.Id, - EmojiName: "smile", - ChannelId: channel2.Id, - }) - require.NoError(t, err) - updatedPost2, err := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false) - require.NoError(t, err) - - // 3 - post deleted with a deleted file - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 3", - FileIds: []string{"test3"}, - }) - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - message3AndFileInfoDeleteAt := model.GetMillis() - err = th.App.Srv().Store().Post().Delete(th.Context, post.Id, message3AndFileInfoDeleteAt, users[0].Id) - require.NoError(t, err) - deletedPost3, err := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, true) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // Message for deleted file -- NOT INCLUDED IN THE BATCH SIZE - attachmentContent := "Hello there message 3" - attachmentPath := "path/to/attachments/file_3.txt" - _, err = attachmentBackend.WriteFile(bytes.NewBufferString(attachmentContent), attachmentPath) - require.NoError(t, err) - info, err2 := th.App.Srv().Store().FileInfo().Save(th.Context, &model.FileInfo{ - Id: st.NewTestID(), - CreatorId: post.UserId, - PostId: post.Id, - CreateAt: post.CreateAt, - UpdateAt: message3AndFileInfoDeleteAt, - Path: attachmentPath, - DeleteAt: message3AndFileInfoDeleteAt, - }) - require.NoError(t, err2) - attachments = append(attachments, info) - time.Sleep(100 * time.Millisecond) - contents = append(contents, attachmentContent) - - // 4 - original post - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 4", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - // 5 - post edited - post, err = th.App.Srv().Store().Post().Update(th.Context, &model.Post{ - Id: post.Id, - CreateAt: post.CreateAt, - EditAt: model.GetMillis(), - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "edited message 4", - }, post) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 6 - post edited but falls on the batch boundary - // original post, but gets modified by the next edit - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 6", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 7 - new post with original message - // update returns "new" post, which is the old post modified - post, err = th.App.Srv().Store().Post().Update(th.Context, &model.Post{ - Id: post.Id, - CreateAt: post.CreateAt, - EditAt: model.GetMillis(), - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "edited message 6", - }, post) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - require.Len(t, posts, 8) - // therefore, need a batch size of 7 - - // use the config fallback - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = start - *cfg.MessageExportSettings.BatchSize = 7 - *cfg.MessageExportSettings.ExportFormat = exportType - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - // check number of messages to be exported - until := model.GetMillis() - assertNumPostsToExport(t, th, 8, start, until) - - // Now run the exports - job := runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - assertNumExported(t, 8, job.Data) - - jobExportDir := job.Data[shared.JobDataExportDir] - jobEndTime, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batchTimes := []batchStartEndTimes{ - {start, posts[7].UpdateAt}, - {posts[7].UpdateAt, jobEndTime}, - } - - // using posts[7] because it's updateAt is what posts[6] is changed to (after the edit) - batch001 := shared.GetBatchPath(jobExportDir, batchTimes[0].start, batchTimes[0].end, 1) - batch002 := shared.GetBatchPath(jobExportDir, batchTimes[1].start, batchTimes[1].end, 2) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches := []string{batch001, batch002} - require.ElementsMatch(t, batches, files) - - return JobResults{ - start: start, - users: users, - joinLeaves: jl, - jobEndTime: jobEndTime, - batches: batches, - posts: posts, - attachments: attachments, - contents: contents, - channels: []*model.Channel{channel2}, - teams: []*model.Team{th.BasicTeam}, - batchTimes: batchTimes, - }, Type3Results{ - message1DeleteAt: message1DeleteAt, - updatedPost2: updatedPost2, - message3AndFileInfoDeleteAt: message3AndFileInfoDeleteAt, - deletedPost3: deletedPost3, - } -} - -func generateE2ETestType4Results(t *testing.T, th *api4.TestHelper, exportType, attachmentDir, exportDir string, - attachmentBackend, exportBackend filestore.FileBackend) JobResults { - start := model.GetMillis() - - // Users: - users := make([]*model.User, 0) - user, err := th.App.Srv().Store().User().Save(th.Context, &model.User{ - Username: "user1", - Email: "user1@email", - }) - require.NoError(t, err) - users = append(users, user) - - // only testing one channel - channel2, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Two", - Name: "channel_two_name", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // user 1 joins before start time and stays (and posts) - user1JoinTime := model.GetMillis() - 200 - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(users[0].Id, channel2.Id, user1JoinTime) - require.NoError(t, err) - - jl := []joinLeave{ - {user1JoinTime, 0}, // user 1 never leaves - } - - // This tests (reading the files exported and testing the exported xml): - // - post edited with 3 simultaneous posts in-between - assertNumPostsToExport(t, th, 0, start, model.GetMillis()) - - var posts []*model.Post - - // 0 - post edited with 3 simultaneous posts in-between - forward - // original post with edited message - originalPost, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 0", - }) - require.NoError(t, err) - posts = append(posts, originalPost) - time.Sleep(100 * time.Millisecond) - - // 1 - edited post - post, err := th.App.Srv().Store().Post().Update(th.Context, &model.Post{ - Id: originalPost.Id, - CreateAt: originalPost.CreateAt, - EditAt: model.GetMillis(), - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "edited message 0", - }, originalPost) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - simultaneous := post.UpdateAt - - // 2 - post 1 at same updateAt - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 2", - CreateAt: simultaneous, - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 3 - post 2 at same updateAt - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 3", - CreateAt: simultaneous, - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // 4 - post 3 in-between - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: users[0].Id, - Message: "message 4", - CreateAt: simultaneous, - }) - require.NoError(t, err) - posts = append(posts, post) - // Use the config fallback for simplicity - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = start - *cfg.MessageExportSettings.BatchSize = 10 - *cfg.MessageExportSettings.ExportFormat = exportType - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - // check number of messages to be exported - until := model.GetMillis() - assertNumPostsToExport(t, th, 5, start, until) - - // Now run the exports - job := runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - // cleanup for next run - _, err = th.App.Srv().Store().Job().Delete(job.Id) - require.NoError(t, err) - - assertNumExported(t, 5, job.Data) - - jobExportDir := job.Data[shared.JobDataExportDir] - jobEndTime, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batchTimes := []batchStartEndTimes{{start, jobEndTime}} - - batch001 := shared.GetBatchPath(jobExportDir, batchTimes[0].start, batchTimes[0].end, 1) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches := []string{batch001} - require.ElementsMatch(t, batches, files) - - return JobResults{ - start: start, - users: users, - joinLeaves: jl, - jobEndTime: jobEndTime, - batches: batches, - posts: posts, - channels: []*model.Channel{channel2}, - teams: []*model.Team{th.BasicTeam}, - batchTimes: batchTimes, - } -} - -// Type5Results specific data needed to be returned by this test only -type Type5Results struct { - message0DeleteAt int64 - updatedPost1 *model.Post - zipBytes [][]byte -} - -// generateE2ETestType5Results does 4 jobs, returns an array of each job's data to use in testing -func generateE2ETestType5Results(t *testing.T, th *api4.TestHelper, exportType, attachmentDir, exportDir string, - attachmentBackend, exportBackend filestore.FileBackend) ([]JobResults, []Type5Results) { - // This tests (reading the files exported and testing the exported file): - // - post deleted in current job: shows created post, then deleted post - // - post deleted in current job but different batch: shows created post (in second batch), then deleted post - // - post created in previous job, deleted in current job: shows only deleted post in current job - // (and same for updated post) - - start := model.GetMillis() - - channel2, err := th.App.Srv().Store().Channel().Save(th.Context, &model.Channel{ - DisplayName: "the Channel Two", - Name: "channel_two_name", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, - }, 999) - require.NoError(t, err) - - // user 1 joins before start time and stays (and posts) - user1JoinTime := model.GetMillis() - 100 - err = th.App.Srv().Store().ChannelMemberHistory().LogJoinEvent(th.BasicUser.Id, channel2.Id, user1JoinTime) - require.NoError(t, err) - - // Job 1: post deleted in current job: shows created post, then deleted post - assertNumPostsToExport(t, th, 0, start, model.GetMillis()) - - var posts []*model.Post - - // post create - post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 0", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // post deleted - message0DeleteAt := model.GetMillis() - err = th.App.Srv().Store().Post().Delete(th.Context, post.Id, message0DeleteAt, th.BasicUser.Id) - require.NoError(t, err) - - // use the config fallback - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.ExportFromTimestamp = start - *cfg.MessageExportSettings.BatchSize = 2 - *cfg.MessageExportSettings.ExportFormat = exportType - *cfg.FileSettings.DriverName = model.ImageDriverLocal - *cfg.FileSettings.Directory = attachmentDir - - if exportDir != attachmentDir { - *cfg.FileSettings.DedicatedExportStore = true - *cfg.FileSettings.ExportDriverName = model.ImageDriverLocal - *cfg.FileSettings.ExportDirectory = exportDir - } - }) - - // check number of messages to be exported -- will be 1 (because one message deleted) - until := model.GetMillis() - assertNumPostsToExport(t, th, 1, start, until) - - // Now run the exports - job := runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - assertNumExported(t, 1, job.Data) - - jobExportDir := job.Data[shared.JobDataExportDir] - jobEndTime, err := strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batchTimes := []batchStartEndTimes{ - {start: start, end: jobEndTime}, - } - - batch001 := shared.GetBatchPath(jobExportDir, batchTimes[0].start, batchTimes[0].end, 1) - files, err := exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches := []string{batch001} - require.ElementsMatch(t, batches, files) - - zipBytes, err := exportBackend.ReadFile(batches[0]) - require.NoError(t, err) - - var setupReturns []JobResults - var setupType5Returns []Type5Results - - setupReturns = append(setupReturns, JobResults{ - posts: posts, - }) - setupType5Returns = append(setupType5Returns, Type5Results{ - message0DeleteAt: message0DeleteAt, - zipBytes: [][]byte{zipBytes}, - }) - - // Cleanup for next job - err = os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - _, err = th.App.Srv().Store().Job().Delete(job.Id) - assert.NoError(t, err) - - // - // Job 2 - // - - // Job 2: post deleted in current job, shows up in second batch because it was deleted after the "second" post - start = model.GetMillis() - - posts = make([]*model.Post, 0) - - // post create -- this will be the one deleted - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 0", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // post create -- this is the "second" post, but it will show up first because first post is deleted after - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 1", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // post deleted -- first post deleted - message0DeleteAt = model.GetMillis() - err = th.App.Srv().Store().Post().Delete(th.Context, posts[0].Id, message0DeleteAt, th.BasicUser.Id) - require.NoError(t, err) - - // use the config fallback - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.ExportFromTimestamp = start - *cfg.MessageExportSettings.BatchSize = 1 - }) - - // check number of messages to be exported - until = model.GetMillis() - assertNumPostsToExport(t, th, 2, start, until) - - // Now run the exports - job = runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - assertNumExported(t, 2, job.Data) - - jobExportDir = job.Data[shared.JobDataExportDir] - jobEndTime, err = strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - // use the message1 updateAt, because the message0's updateAt is now after - batch001 = shared.GetBatchPath(jobExportDir, start, posts[1].UpdateAt, 1) - batch002 := shared.GetBatchPath(jobExportDir, posts[1].UpdateAt, jobEndTime, 2) - files, err = exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches = []string{batch001, batch002} - require.ElementsMatch(t, batches, files) - - zipBytes, err = exportBackend.ReadFile(batches[0]) - require.NoError(t, err) - zipBytes2, err := exportBackend.ReadFile(batches[1]) - require.NoError(t, err) - - setupReturns = append(setupReturns, JobResults{ - posts: posts, - }) - setupType5Returns = append(setupType5Returns, Type5Results{ - message0DeleteAt: message0DeleteAt, - zipBytes: [][]byte{zipBytes, zipBytes2}, - }) - - // Cleanup for next job - err = os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - _, err = th.App.Srv().Store().Job().Delete(job.Id) - assert.NoError(t, err) - - // - // Job 3 - // - - // Job 3: post created in previous job, deleted in current job: shows only deleted post in current job - start = model.GetMillis() - - posts = make([]*model.Post, 0) - - // post create -- this will be the one deleted in second job - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 0", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // post create -- this will be the one updated in second job - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 1", - }) - require.NoError(t, err) - posts = append(posts, post) - - // use the config fallback - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.ExportFromTimestamp = start - *cfg.MessageExportSettings.BatchSize = 10 - }) - - until = model.GetMillis() - assertNumPostsToExport(t, th, 2, start, until) - - // Now run the exports - job = runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - assertNumExported(t, 2, job.Data) - - jobExportDir = job.Data[shared.JobDataExportDir] - jobEndTime, err = strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batch001 = shared.GetBatchPath(jobExportDir, start, jobEndTime, 1) - files, err = exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches = []string{batch001} - require.ElementsMatch(t, batches, files) - - zipBytes, err = exportBackend.ReadFile(batches[0]) - require.NoError(t, err) - - setupReturns = append(setupReturns, JobResults{ - posts: posts, - }) - setupType5Returns = append(setupType5Returns, Type5Results{ - zipBytes: [][]byte{zipBytes}, - }) - - // Now, clean up outputs for next job - err = os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - _, err = th.App.Srv().Store().Job().Delete(job.Id) - assert.NoError(t, err) - - // - // Job 4 - // - start = model.GetMillis() - - // make a copy of posts so the one we sent earlier doesn't get modified and cause difficult to detect bugs - posts = append([]*model.Post{}, posts...) - - // post create -- filler - post, err = th.App.Srv().Store().Post().Save(th.Context, &model.Post{ - ChannelId: channel2.Id, - UserId: th.BasicUser.Id, - Message: "message 1", - }) - require.NoError(t, err) - posts = append(posts, post) - time.Sleep(100 * time.Millisecond) - - // post deleted -- first post deleted (the first one exported earlier) - message0DeleteAt = model.GetMillis() - err = th.App.Srv().Store().Post().Delete(th.Context, posts[0].Id, message0DeleteAt, th.BasicUser.Id) - require.NoError(t, err) - - // post updated -- second post updated (the second one exported earlier) - _, err = th.App.Srv().Store().Reaction().Save(&model.Reaction{ - UserId: th.BasicUser.Id, - PostId: posts[1].Id, - EmojiName: "smile", - ChannelId: channel2.Id, - }) - require.NoError(t, err) - updatedPost1, err := th.App.Srv().Store().Post().GetSingle(th.Context, posts[1].Id, false) - require.NoError(t, err) - - // use the config fallback - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.ExportFromTimestamp = start - }) - - // check number of messages to be exported - // filler post, deleted post, and updated post - until = model.GetMillis() - assertNumPostsToExport(t, th, 3, start, until) - - // Now run the exports - job = runJobForTest(t, th, map[string]string{shared.JobDataJobEndTime: strconv.FormatInt(until, 10)}) - - assertNumExported(t, 3, job.Data) - - jobExportDir = job.Data[shared.JobDataExportDir] - jobEndTime, err = strconv.ParseInt(job.Data[shared.JobDataJobEndTime], 10, 64) - require.NoError(t, err) - - batch001 = shared.GetBatchPath(jobExportDir, start, jobEndTime, 1) - files, err = exportBackend.ListDirectory(jobExportDir) - require.NoError(t, err) - batches = []string{batch001} - require.ElementsMatch(t, batches, files) - - zipBytes, err = exportBackend.ReadFile(batches[0]) - require.NoError(t, err) - - setupReturns = append(setupReturns, JobResults{ - posts: posts, - }) - setupType5Returns = append(setupType5Returns, Type5Results{ - message0DeleteAt: message0DeleteAt, - updatedPost1: updatedPost1, - zipBytes: [][]byte{zipBytes}, - }) - - // Cleanup - err = os.RemoveAll(exportDir) - assert.NoError(t, err) - err = os.RemoveAll(attachmentDir) - assert.NoError(t, err) - _, err = th.App.Srv().Store().Job().Delete(job.Id) - assert.NoError(t, err) - - return setupReturns, setupType5Returns -} diff --git a/server/enterprise/message_export/scheduler.go b/server/enterprise/message_export/scheduler.go deleted file mode 100644 index 99aa440d7e2..00000000000 --- a/server/enterprise/message_export/scheduler.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "net/http" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/jobs" - ejobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" -) - -type MessageExportScheduler struct { - jobServer *jobs.JobServer - enabledFunc func(cfg *model.Config) bool -} - -var _ jobs.Scheduler = (*MessageExportScheduler)(nil) - -func NewMessageExportScheduler(jobServer *jobs.JobServer, enabledFunc func(cfg *model.Config) bool) *MessageExportScheduler { - return &MessageExportScheduler{ - enabledFunc: enabledFunc, - jobServer: jobServer, - } -} - -func (s *MessageExportScheduler) Enabled(cfg *model.Config) bool { - return s.enabledFunc(cfg) -} - -func (s *MessageExportScheduler) NextScheduleTime(cfg *model.Config, now time.Time, _ bool, _ *model.Job) *time.Time { - // We set the next scheduled time regardless of whether there is a running or pending job - // In ScheduleJob we check pending or running jobs, before actually scheduling a job - parsedTime, err := time.Parse("15:04", *cfg.MessageExportSettings.DailyRunTime) - if err != nil { - s.jobServer.Logger().Error( - "Cannot determine next schedule time for message export. DailyRunTime config value is invalid.", - mlog.String("daily_run_time", *cfg.MessageExportSettings.DailyRunTime), - ) - return nil - } - return jobs.GenerateNextStartDateTime(now, parsedTime) -} - -func (s *MessageExportScheduler) ScheduleJob(rctx request.CTX, _ *model.Config, havePendingJobs bool, _ *model.Job) (*model.Job, *model.AppError) { - // Don't schedule a job if we already have a pending job - if havePendingJobs { - return nil, nil - } - // Don't schedule a job if we already have a running job - count, err := s.jobServer.Store.Job().GetCountByStatusAndType(model.JobStatusInProgress, model.JobTypeMessageExport) - if err != nil { - return nil, model.NewAppError( - "ScheduleJob", - "app.job.get_count_by_status_and_type.app_error", - map[string]any{"jobtype": model.JobTypeMessageExport, "status": model.JobStatusInProgress}, - "", - http.StatusInternalServerError).Wrap(err) - } - if count > 0 { - return nil, nil - } - return s.jobServer.CreateJob(rctx, model.JobTypeMessageExport, nil) -} - -func (dr *MessageExportJobInterfaceImpl) MakeScheduler() ejobs.Scheduler { - enabled := func(cfg *model.Config) bool { - license := dr.Server.License() - return license != nil && *license.Features.MessageExport && *cfg.MessageExportSettings.EnableExport - } - return NewMessageExportScheduler(dr.Server.Jobs, enabled) -} diff --git a/server/enterprise/message_export/scheduler_test.go b/server/enterprise/message_export/scheduler_test.go deleted file mode 100644 index 5371a12c778..00000000000 --- a/server/enterprise/message_export/scheduler_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/api4" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" -) - -func TestMessageExportJobEnabled(t *testing.T) { - t.Run("MessageExport job is enabled only if feature is enabled", func(t *testing.T) { - th := api4.SetupEnterpriseWithStoreMock(t) - defer th.TearDown() - - th.Server.SetLicense(model.NewTestLicense("message_export")) - - messageExport := &MessageExportJobInterfaceImpl{th.App.Srv()} - - config := &model.Config{ - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - }, - } - scheduler := messageExport.MakeScheduler() - result := scheduler.Enabled(config) - assert.True(t, result) - }) - - t.Run("MessageExport job is disabled if there is no license", func(t *testing.T) { - th := api4.SetupEnterpriseWithStoreMock(t) - defer th.TearDown() - - th.Server.SetLicense(nil) - - messageExport := &MessageExportJobInterfaceImpl{th.App.Srv()} - - config := &model.Config{ - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - }, - } - scheduler := messageExport.MakeScheduler() - result := scheduler.Enabled(config) - assert.False(t, result) - }) -} - -func TestMessageExportJobPending(t *testing.T) { - th := api4.SetupEnterpriseWithStoreMock(t) - defer th.TearDown() - - mockStore := th.App.Srv().Platform().Store.(*mocks.Store) - mockUserStore := mocks.UserStore{} - mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - mockPostStore := mocks.PostStore{} - mockPostStore.On("GetMaxPostSize").Return(65535, nil) - mockSystemStore := mocks.SystemStore{} - mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil) - mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil) - mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil) - mockStore.On("User").Return(&mockUserStore) - mockStore.On("Post").Return(&mockPostStore) - mockStore.On("System").Return(&mockSystemStore) - mockStore.On("GetDBSchemaVersion").Return(1, nil) - - mockJobServerStore := th.App.Srv().Jobs.Store.(*mocks.Store) - mockJobStore := mocks.JobStore{} - // Mock that we have an in-progress message export job - mockJobStore.On("GetCountByStatusAndType", model.JobStatusInProgress, model.JobTypeMessageExport).Return(int64(1), nil) - mockJobServerStore.On("Job").Return(&mockJobStore) - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.MessageExportSettings.EnableExport = true - *cfg.MessageExportSettings.DailyRunTime = "10:40" - }) - - th.App.Srv().SetLicense(model.NewTestLicense("message_export")) - - messageExport := &MessageExportJobInterfaceImpl{th.App.Srv()} - scheduler := messageExport.MakeScheduler() - - // Confirm that job is not scheduled if we have pending jobs - job, err := scheduler.ScheduleJob(th.Context, th.App.Config(), true, nil) - assert.Nil(t, err) - assert.Nil(t, job) - - // Confirm that job is not scheduled if we have an inprogress job - job, err = scheduler.ScheduleJob(th.Context, th.App.Config(), false, nil) - assert.Nil(t, err) - assert.Nil(t, job) -} diff --git a/server/enterprise/message_export/testdata/actianceE2E1Batch1ch2.tmpl b/server/enterprise/message_export/testdata/actianceE2E1Batch1ch2.tmpl deleted file mode 100644 index 78c9bbae2fd..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E1Batch1ch2.tmpl +++ /dev/null @@ -1,101 +0,0 @@ - - private - channel_two_name - %s - %d - - user1@email - user - %d - user1@email - - - user2@email - user - %d - user2@email - - - user3@email - user - %d - user3@email - - - %s - user1@email - user - %d - message 0 - - - user1@email - %d - - path/to/attachments/file_0.txt - - - user1@email - %d - - path/to/attachments/file_0.txt - Completed - - - %s - user1@email - user - %d - message 1 - - - user1@email - %d - - path/to/attachments/file_1.txt - - - user1@email - %d - - path/to/attachments/file_1.txt - Completed - - - %s - user1@email - user - %d - message 2 - - - user1@email - %d - - path/to/attachments/file_2.txt - - - user1@email - %d - - path/to/attachments/file_2.txt - Completed - - - user2@email - user - %d - user2@email - - - user3@email - user - %d - user3@email - - - user1@email - user - %d - user1@email - - %d - diff --git a/server/enterprise/message_export/testdata/actianceE2E1Batch1ch3.tmpl b/server/enterprise/message_export/testdata/actianceE2E1Batch1ch3.tmpl deleted file mode 100644 index 2a246c4690a..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E1Batch1ch3.tmpl +++ /dev/null @@ -1,17 +0,0 @@ - - public - channel_three_name - %s - %d - - user7@email - user - %d - user7@email - - - user7@email - user - %d - user7@email - - %d - diff --git a/server/enterprise/message_export/testdata/actianceE2E1Batch2.tmpl b/server/enterprise/message_export/testdata/actianceE2E1Batch2.tmpl deleted file mode 100644 index 696344dfc3e..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E1Batch2.tmpl +++ /dev/null @@ -1,92 +0,0 @@ - - - - private - channel_two_name - %s - %d - - user1@email - user - %d - user1@email - - - user4@email - user - %d - user4@email - - - %s - user1@email - user - %d - message 3 - - - user1@email - %d - - path/to/attachments/file_3.txt - - - user1@email - %d - - path/to/attachments/file_3.txt - Completed - - - %s - user1@email - user - %d - message 4 - - - user1@email - %d - - path/to/attachments/file_4.txt - - - user1@email - %d - - path/to/attachments/file_4.txt - Completed - - - %s - user1@email - user - %d - message 5 - - - user1@email - %d - - path/to/attachments/file_5.txt - - - user1@email - %d - - path/to/attachments/file_5.txt - Completed - - - user4@email - user - %d - user4@email - - - user1@email - user - %d - user1@email - - %d - - diff --git a/server/enterprise/message_export/testdata/actianceE2E1Batch3ch2.tmpl b/server/enterprise/message_export/testdata/actianceE2E1Batch3ch2.tmpl deleted file mode 100644 index a3050015e29..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E1Batch3ch2.tmpl +++ /dev/null @@ -1,101 +0,0 @@ - - private - channel_two_name - %s - %d - - user1@email - user - %d - user1@email - - - user5@email - user - %d - user5@email - - - user6@email - user - %d - user6@email - - - %s - user1@email - user - %d - message 6 - - - user1@email - %d - - path/to/attachments/file_6.txt - - - user1@email - %d - - path/to/attachments/file_6.txt - Completed - - - %s - user1@email - user - %d - message 7 - - - user1@email - %d - - path/to/attachments/file_7.txt - - - user1@email - %d - - path/to/attachments/file_7.txt - Completed - - - %s - user1@email - user - %d - message 8 - - - user1@email - %d - - path/to/attachments/file_8.txt - - - user1@email - %d - - path/to/attachments/file_8.txt - Completed - - - user5@email - user - %d - user5@email - - - user6@email - user - %d - user6@email - - - user1@email - user - %d - user1@email - - %d - diff --git a/server/enterprise/message_export/testdata/actianceE2E1Batch3ch4.tmpl b/server/enterprise/message_export/testdata/actianceE2E1Batch3ch4.tmpl deleted file mode 100644 index 28f2a25217f..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E1Batch3ch4.tmpl +++ /dev/null @@ -1,17 +0,0 @@ - - public - channel_four_name - %s - %d - - user8@email - user - %d - user8@email - - - user8@email - user - %d - user8@email - - %d - diff --git a/server/enterprise/message_export/testdata/actianceE2E2.tmpl b/server/enterprise/message_export/testdata/actianceE2E2.tmpl deleted file mode 100644 index 6e7e0042a40..00000000000 --- a/server/enterprise/message_export/testdata/actianceE2E2.tmpl +++ /dev/null @@ -1,46 +0,0 @@ - - - - private - channel_two_name - %s - %d - - user1@email - user - %d - user1@email - - - user2@email - user - %d - user2@email - - - %s - user1@email - user - %d - message 1 - - - %s - user2@email - user - %d - message 2 - - - user1@email - user - %d - user1@email - - - user2@email - user - %d - user2@email - - %d - - diff --git a/server/enterprise/message_export/testdata/actianceXMLHeader.tmpl b/server/enterprise/message_export/testdata/actianceXMLHeader.tmpl deleted file mode 100644 index 00082370b95..00000000000 --- a/server/enterprise/message_export/testdata/actianceXMLHeader.tmpl +++ /dev/null @@ -1,3 +0,0 @@ - - -%s%s diff --git a/server/enterprise/message_export/testdata/csvE2E1Batch1.tmpl b/server/enterprise/message_export/testdata/csvE2E1Batch1.tmpl deleted file mode 100644 index f2c741c78ed..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E1Batch1.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[16]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[17]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[7]s,user2@email,user2,,,,User user2 (user2@email) joined the channel,enter,user, -%[18]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[7]s,user2@email,user2,,,,User user2 (user2@email) left the channel,leave,user, -%[19]d,%[19]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[10]s,,,message 0,message,user, -%[19]d,%[19]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[10]s,,,(files/%[10]s/%[13]s-file_0.txt),attachment,user, -%[20]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[8]s,user3@email,user3,,,,User user3 (user3@email) joined the channel,enter,user, -%[21]d,%[21]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[11]s,,,message 1,message,user, -%[21]d,%[21]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[11]s,,,(files/%[11]s/%[14]s-file_1.txt),attachment,user, -%[22]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[8]s,user3@email,user3,,,,User user3 (user3@email) left the channel,leave,user, -%[23]d,0,,%[1]s,,,%[5]s,channel_three_name,the Channel Three,public,%[9]s,user7@email,user7,,,,User user7 (user7@email) was already in the channel,previously-joined,user, -%[24]d,0,,%[1]s,,,%[5]s,channel_three_name,the Channel Three,public,%[9]s,user7@email,user7,,,,User user7 (user7@email) left the channel,leave,user, -%[25]d,%[25]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[12]s,,,message 2,message,user, -%[25]d,%[25]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[12]s,,,(files/%[12]s/%[15]s-file_2.txt),attachment,user, diff --git a/server/enterprise/message_export/testdata/csvE2E1Batch2.tmpl b/server/enterprise/message_export/testdata/csvE2E1Batch2.tmpl deleted file mode 100644 index 0d95a21e230..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E1Batch2.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[13]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[14]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user4@email,user4,,,,User user4 (user4@email) joined the channel,enter,user, -%[15]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user4@email,user4,,,,User user4 (user4@email) left the channel,leave,user, -%[16]d,%[16]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 3,message,user, -%[16]d,%[16]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,(files/%[7]s/%[10]s-file_3.txt),attachment,user, -%[17]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 4,message,user, -%[17]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,(files/%[8]s/%[11]s-file_4.txt),attachment,user, -%[18]d,%[18]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 5,message,user, -%[18]d,%[18]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[12]s-file_5.txt),attachment,user, diff --git a/server/enterprise/message_export/testdata/csvE2E1Batch3.tmpl b/server/enterprise/message_export/testdata/csvE2E1Batch3.tmpl deleted file mode 100644 index 4eb6ab76254..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E1Batch3.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[16]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[17]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[7]s,user5@email,user5,,,,User user5 (user5@email) joined the channel,enter,user, -%[18]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[7]s,user5@email,user5,,,,User user5 (user5@email) left the channel,leave,user, -%[19]d,%[19]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[10]s,,,message 6,message,user, -%[19]d,%[19]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[10]s,,,(files/%[10]s/%[13]s-file_6.txt),attachment,user, -%[20]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[11]s,,,message 7,message,user, -%[20]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[11]s,,,(files/%[11]s/%[14]s-file_7.txt),attachment,user, -%[21]d,%[21]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[12]s,,,message 8,message,user, -%[21]d,%[21]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user1@email,user1,%[12]s,,,(files/%[12]s/%[15]s-file_8.txt),attachment,user, -%[22]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[8]s,user6@email,user6,,,,User user6 (user6@email) joined the channel,enter,user, -%[23]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[8]s,user6@email,user6,,,,User user6 (user6@email) left the channel,leave,user, -%[24]d,0,,%[1]s,,,%[5]s,channel_four_name,the Channel Four,public,%[9]s,user8@email,user8,,,,User user8 (user8@email) joined the channel,enter,user, diff --git a/server/enterprise/message_export/testdata/csvE2E2Batch1.tmpl b/server/enterprise/message_export/testdata/csvE2E2Batch1.tmpl deleted file mode 100644 index bd0250d5daa..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E2Batch1.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[9]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[10]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user2@email,user2,,,,User user2 (user2@email) was already in the channel,previously-joined,user, -%[11]d,%[11]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 1,message,user, -%[12]d,%[12]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[6]s,user2@email,user2,%[8]s,,,message 2,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm1.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch1Perm1.tmpl deleted file mode 100644 index e879f4000d4..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm1.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[14]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[15]d,%[15]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,,,message 0,message,user, -%[16]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 1,message,user, -%[16]d,%[17]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,delete message 1,message,user, -%[18]d,%[25]d,UpdatedNoMsgChange,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 2,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 3,message,user, -%[19]d,%[20]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,delete message 3,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),attachment,user, -%[19]d,%[20]d,FileDeleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),deleted attachment,user, -%[21]d,%[22]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[10]s,%[11]s,,message 4,message,user, -%[21]d,%[22]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[11]s,,,edited message 4,message,user, -%[23]d,%[26]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[12]s,%[24]s,,message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm2.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch1Perm2.tmpl deleted file mode 100644 index 15166d1fa33..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm2.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[14]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[15]d,%[15]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,,,message 0,message,user, -%[16]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 1,message,user, -%[16]d,%[17]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,delete message 1,message,user, -%[18]d,%[25]d,UpdatedNoMsgChange,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 2,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 3,message,user, -%[19]d,%[20]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,delete message 3,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),attachment,user, -%[19]d,%[20]d,FileDeleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),deleted attachment,user, -%[21]d,%[22]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[11]s,,,edited message 4,message,user, -%[21]d,%[22]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[10]s,%[11]s,,message 4,message,user, -%[23]d,%[26]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[12]s,%[24]s,,message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm3.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch1Perm3.tmpl deleted file mode 100644 index 0ff6103a4ca..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm3.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[14]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[15]d,%[15]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,,,message 0,message,user, -%[16]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 1,message,user, -%[16]d,%[17]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,delete message 1,message,user, -%[18]d,%[24]d,UpdatedNoMsgChange,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 2,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 3,message,user, -%[19]d,%[20]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,delete message 3,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),attachment,user, -%[19]d,%[20]d,FileDeleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),deleted attachment,user, -%[21]d,%[22]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[10]s,%[11]s,,message 4,message,user, -%[21]d,%[22]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[11]s,,,edited message 4,message,user, -%[25]d,%[26]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[12]s,,,edited message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm4.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch1Perm4.tmpl deleted file mode 100644 index ba121187d74..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch1Perm4.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[14]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[15]d,%[15]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,,,message 0,message,user, -%[16]d,%[17]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,message 1,message,user, -%[16]d,%[17]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,delete message 1,message,user, -%[18]d,%[24]d,UpdatedNoMsgChange,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 2,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 3,message,user, -%[19]d,%[20]d,Deleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,delete message 3,message,user, -%[19]d,%[20]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),attachment,user, -%[19]d,%[20]d,FileDeleted,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,(files/%[9]s/%[13]s-file_3.txt),deleted attachment,user, -%[21]d,%[22]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[11]s,,,edited message 4,message,user, -%[21]d,%[22]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[10]s,%[11]s,,message 4,message,user, -%[25]d,%[26]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[12]s,,,edited message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch2Perm1.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch2Perm1.tmpl deleted file mode 100644 index aba0e2a3475..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch2Perm1.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[9]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[8]d,%[10]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,%[7]s,,message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E3Batch2Perm2.tmpl b/server/enterprise/message_export/testdata/csvE2E3Batch2Perm2.tmpl deleted file mode 100644 index cbf6719310b..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E3Batch2Perm2.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -Post Creation Time,Post Update Time,Post Update Type,Team Id,Team Name,Team Display Name,Channel Id,Channel Name,Channel Display Name,Channel Type,User Id,User Email,Username,Post Id,Edited By Post Id,Replied to Post Id,Post Message,Post Type,User Type,Previews Post Id -%[8]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[7]d,%[9]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,,,edited message 6,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E4Batch1.tmpl b/server/enterprise/message_export/testdata/csvE2E4Batch1.tmpl deleted file mode 100644 index 3fbf4f31bce..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E4Batch1.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -%[11]d,0,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,,,,User user1 (user1@email) was already in the channel,previously-joined,user, -%[12]d,%[14]d,EditedOriginalMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[6]s,%[7]s,,message 0,message,user, -%[12]d,%[14]d,EditedNewMsg,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[7]s,,,edited message 0,message,user, -%[13]d,%[14]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[8]s,,,message 2,message,user, -%[13]d,%[14]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[9]s,,,message 3,message,user, -%[13]d,%[14]d,,%[1]s,%[2]s,%[3]s,%[4]s,channel_two_name,the Channel Two,private,%[5]s,user1@email,user1,%[10]s,,,message 4,message,user, diff --git a/server/enterprise/message_export/testdata/csvE2E5Job1.tmpl b/server/enterprise/message_export/testdata/csvE2E5Job1.tmpl deleted file mode 100644 index b803ead72b7..00000000000 --- a/server/enterprise/message_export/testdata/csvE2E5Job1.tmpl +++ /dev/null @@ -1,2 +0,0 @@ -1732245676086,czoko3u8oidrtcgt58qi3afmrc,faketeambfc64t,dn_qtkwchcc7jng5mo87qw5gzxojo,zao11dtduinninqwtbjn3c6jzw,fakechannelsucatskrmt,dn_pyo5j7yqui865qt69ikoi6sgcr,public,741xw3i71frkmks64of8myh4gc,tfxgyrwfbbymzqz3c74sysbqao@localhost,fakeuser6b4riuzx5b,pf4y8d9s77domftewwfzk4fn5o,,,message 0,message,user, -1732245676088,czoko3u8oidrtcgt58qi3afmrc,faketeambfc64t,dn_qtkwchcc7jng5mo87qw5gzxojo,zao11dtduinninqwtbjn3c6jzw,fakechannelsucatskrmt,dn_pyo5j7yqui865qt69ikoi6sgcr,public,741xw3i71frkmks64of8myh4gc,tfxgyrwfbbymzqz3c74sysbqao@localhost,fakeuser6b4riuzx5b,pf4y8d9s77domftewwfzk4fn5o,,,delete message 0,message,user, diff --git a/server/enterprise/message_export/testdata/grE2E1Batch1.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch1.tmpl deleted file mode 100644 index abbac799f95..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch1.tmpl +++ /dev/null @@ -1,137 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[16]s@user1useruser1@email%[3]s%[4]s0 seconds3
    %[17]s@user2useruser2@email%[5]s%[6]s0 seconds0
    %[18]s@user3useruser3@email%[7]s%[8]s0 seconds0
    - -

    Messages

    -
    - -
    - -

    Exported on %[12]s

    diff --git a/server/enterprise/message_export/testdata/grE2E1Batch1Ch3.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch1Ch3.tmpl deleted file mode 100644 index 3461fba402c..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch1Ch3.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[5]s@user7useruser7@email%[6]s%[7]s0 seconds0
    - -

    Messages

    -
    - -
    - -

    Exported on %[8]s

    diff --git a/server/enterprise/message_export/testdata/grE2E1Batch1Summary.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch1Summary.tmpl deleted file mode 100644 index 9855c22aafb..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch1Summary.tmpl +++ /dev/null @@ -1,29 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[14]s -* TeamName: %[15]s -* TeamDisplayName: %[16]s -* ChannelId: %[13]s -* ChannelName: channel_two_name -* ChannelDisplayName: %[1]s -* Started: %[2]s -* Ended: %[3]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[17]s @user1 user user1@email %[4]s %[5]s 0 seconds 3 %[18]s @user2 user user2@email %[6]s %[7]s 0 seconds 0 %[19]s @user3 user user3@email %[8]s %[9]s 0 seconds 0 - --------- -Messages --------- - -* %[20]s %[10]s @user1 %[17]s @user1 user (user1@email) message 0 -* %[20]s %[10]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[21]s %[11]s @user1 %[17]s @user1 user (user1@email) message 1 -* %[21]s %[11]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[22]s %[12]s @user1 %[17]s @user1 user (user1@email) message 2 -* %[22]s %[12]s @user1 %[17]s @user1 user (user1@email) Uploaded file diff --git a/server/enterprise/message_export/testdata/grE2E1Batch1SummaryCh3.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch1SummaryCh3.tmpl deleted file mode 100644 index ecc9eba10f2..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch1SummaryCh3.tmpl +++ /dev/null @@ -1,22 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[1]s -* TeamName: -* TeamDisplayName: -* ChannelId: %[4]s -* ChannelName: channel_three_name -* ChannelDisplayName: the Channel Three -* Started: %[5]s -* Ended: %[6]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[7]s @user7 user user7@email %[8]s %[9]s 0 seconds 0 - --------- -Messages --------- diff --git a/server/enterprise/message_export/testdata/grE2E1Batch2.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch2.tmpl deleted file mode 100644 index 81cea566453..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch2.tmpl +++ /dev/null @@ -1,126 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[14]s@user1useruser1@email%[3]s%[4]s0 seconds3
    %[16]s@user4useruser4@email%[5]s%[6]s0 seconds0
    - -

    Messages

    -
    - -
    - -

    Exported on %[10]s

    diff --git a/server/enterprise/message_export/testdata/grE2E1Batch2Summary.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch2Summary.tmpl deleted file mode 100644 index 5197e603df7..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch2Summary.tmpl +++ /dev/null @@ -1,29 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[11]s -* TeamName: %[12]s -* TeamDisplayName: %[13]s -* ChannelId: %[10]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[14]s @user1 user user1@email %[3]s %[4]s 0 seconds 3 %[15]s @user4 user user4@email %[5]s %[6]s 0 seconds 0 - --------- -Messages --------- - -* %[16]s %[7]s @user1 %[14]s @user1 user (user1@email) message 3 -* %[16]s %[7]s @user1 %[14]s @user1 user (user1@email) Uploaded file -* %[17]s %[8]s @user1 %[14]s @user1 user (user1@email) message 4 -* %[17]s %[8]s @user1 %[14]s @user1 user (user1@email) Uploaded file -* %[18]s %[9]s @user1 %[14]s @user1 user (user1@email) message 5 -* %[18]s %[9]s @user1 %[14]s @user1 user (user1@email) Uploaded file diff --git a/server/enterprise/message_export/testdata/grE2E1Batch3.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch3.tmpl deleted file mode 100644 index 3aa981c8ad6..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch3.tmpl +++ /dev/null @@ -1,137 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[16]s@user1useruser1@email%[3]s%[4]s0 seconds3
    %[17]s@user5useruser5@email%[5]s%[6]s0 seconds0
    %[18]s@user6useruser6@email%[7]s%[8]s0 seconds0
    - -

    Messages

    -
    - -
    - -

    Exported on %[12]s

    diff --git a/server/enterprise/message_export/testdata/grE2E1Batch3Ch4.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch3Ch4.tmpl deleted file mode 100644 index a24fc616f90..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch3Ch4.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[5]s@user8useruser8@email%[6]s%[4]s0 seconds0
    - -

    Messages

    -
    - -
    - -

    Exported on %[8]s

    diff --git a/server/enterprise/message_export/testdata/grE2E1Batch3Summary.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch3Summary.tmpl deleted file mode 100644 index 2554298f024..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch3Summary.tmpl +++ /dev/null @@ -1,29 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[13]s -* TeamName: %[14]s -* TeamDisplayName: %[15]s -* ChannelId: %[12]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[16]s @user1 user user1@email %[3]s %[4]s 0 seconds 3 %[22]s @user5 user user5@email %[5]s %[6]s 0 seconds 0 %[23]s @user6 user user6@email %[7]s %[8]s 0 seconds 0 - --------- -Messages --------- - -* %[19]s %[9]s @user1 %[16]s @user1 user (user1@email) message 6 -* %[19]s %[9]s @user1 %[16]s @user1 user (user1@email) Uploaded file -* %[20]s %[10]s @user1 %[16]s @user1 user (user1@email) message 7 -* %[20]s %[10]s @user1 %[16]s @user1 user (user1@email) Uploaded file -* %[21]s %[11]s @user1 %[16]s @user1 user (user1@email) message 8 -* %[21]s %[11]s @user1 %[16]s @user1 user (user1@email) Uploaded file diff --git a/server/enterprise/message_export/testdata/grE2E1Batch3SummaryCh4.tmpl b/server/enterprise/message_export/testdata/grE2E1Batch3SummaryCh4.tmpl deleted file mode 100644 index 910dafdda96..00000000000 --- a/server/enterprise/message_export/testdata/grE2E1Batch3SummaryCh4.tmpl +++ /dev/null @@ -1,22 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[1]s -* TeamName: -* TeamDisplayName: -* ChannelId: %[4]s -* ChannelName: channel_four_name -* ChannelDisplayName: the Channel Four -* Started: %[5]s -* Ended: %[6]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[7]s @user8 user user8@email %[8]s %[6]s 0 seconds 0 - --------- -Messages --------- diff --git a/server/enterprise/message_export/testdata/grE2E2Batch1.tmpl b/server/enterprise/message_export/testdata/grE2E2Batch1.tmpl deleted file mode 100644 index 43c641755c7..00000000000 --- a/server/enterprise/message_export/testdata/grE2E2Batch1.tmpl +++ /dev/null @@ -1,82 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[14]s@user1useruser1@email%[3]s%[4]s0 seconds1
    %[15]s@user2useruser2@email%[5]s%[6]s0 seconds1
    - -

    Messages

    -
    - -
    - -

    Exported on %[9]s

    diff --git a/server/enterprise/message_export/testdata/grE2E2Batch1Summary.tmpl b/server/enterprise/message_export/testdata/grE2E2Batch1Summary.tmpl deleted file mode 100644 index 8dbea6c4ea3..00000000000 --- a/server/enterprise/message_export/testdata/grE2E2Batch1Summary.tmpl +++ /dev/null @@ -1,25 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[9]s -* TeamName: %[10]s -* TeamDisplayName: %[11]s -* ChannelId: %[12]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[13]s @user1 user user1@email %[3]s %[4]s 0 seconds 1 %[14]s @user2 user user2@email %[5]s %[6]s 0 seconds 1 - --------- -Messages --------- - -* %[15]s %[7]s @user1 %[13]s @user1 user (user1@email) message 1 -* %[16]s %[8]s @user2 %[14]s @user2 user (user2@email) message 2 diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1Perm1.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1Perm1.tmpl deleted file mode 100644 index f3e66c0c1cc..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1Perm1.tmpl +++ /dev/null @@ -1,191 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[17]s@user1useruser1@email%[3]s%[4]s0 seconds9
    - -

    Messages

    -
    - -
    - -

    Exported on %[1]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1Perm2.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1Perm2.tmpl deleted file mode 100644 index c755f39a459..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1Perm2.tmpl +++ /dev/null @@ -1,191 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[17]s@user1useruser1@email%[3]s%[4]s0 seconds9
    - -

    Messages

    -
    - -
    - -

    Exported on %[1]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1Perm3.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1Perm3.tmpl deleted file mode 100644 index 70fbaf1602f..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1Perm3.tmpl +++ /dev/null @@ -1,191 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[17]s@user1useruser1@email%[3]s%[4]s0 seconds9
    - -

    Messages

    -
    - -
    - -

    Exported on %[1]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1Perm4.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1Perm4.tmpl deleted file mode 100644 index 118aa327252..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1Perm4.tmpl +++ /dev/null @@ -1,191 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[17]s@user1useruser1@email%[3]s%[4]s0 seconds9
    - -

    Messages

    -
    - -
    - -

    Exported on %[1]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm1.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm1.tmpl deleted file mode 100644 index ef64694bf1f..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm1.tmpl +++ /dev/null @@ -1,34 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[13]s -* TeamName: %[14]s -* TeamDisplayName: %[15]s -* ChannelId: %[16]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[17]s @user1 user user1@email %[3]s %[4]s 0 seconds 9 - --------- -Messages --------- - -* %[18]s %[5]s @user1 %[17]s @user1 user (user1@email) message 0 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) message 1 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) delete message 1 Deleted %[26]s -* %[20]s %[7]s @user1 %[17]s @user1 user (user1@email) message 2 UpdatedNoMsgChange %[27]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) message 3 -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) delete message 3 Deleted %[9]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Deleted file FileDeleted %[9]s -* %[22]s %[10]s @user1 %[17]s @user1 user (user1@email) message 4 EditedOriginalMsg %[28]s %[23]s -* %[23]s %[11]s @user1 %[17]s @user1 user (user1@email) edited message 4 EditedNewMsg %[28]s -* %[24]s %[12]s @user1 %[17]s @user1 user (user1@email) message 6 EditedOriginalMsg %[29]s %[25]s diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm2.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm2.tmpl deleted file mode 100644 index 8d63a411856..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm2.tmpl +++ /dev/null @@ -1,34 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[13]s -* TeamName: %[14]s -* TeamDisplayName: %[15]s -* ChannelId: %[16]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[17]s @user1 user user1@email %[3]s %[4]s 0 seconds 9 - --------- -Messages --------- - -* %[18]s %[5]s @user1 %[17]s @user1 user (user1@email) message 0 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) message 1 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) delete message 1 Deleted %[26]s -* %[20]s %[7]s @user1 %[17]s @user1 user (user1@email) message 2 UpdatedNoMsgChange %[27]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) message 3 -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) delete message 3 Deleted %[9]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Deleted file FileDeleted %[9]s -* %[23]s %[11]s @user1 %[17]s @user1 user (user1@email) edited message 4 EditedNewMsg %[28]s -* %[22]s %[10]s @user1 %[17]s @user1 user (user1@email) message 4 EditedOriginalMsg %[28]s %[23]s -* %[24]s %[12]s @user1 %[17]s @user1 user (user1@email) message 6 EditedOriginalMsg %[29]s %[25]s diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm3.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm3.tmpl deleted file mode 100644 index b396e09cfc9..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm3.tmpl +++ /dev/null @@ -1,34 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[13]s -* TeamName: %[14]s -* TeamDisplayName: %[15]s -* ChannelId: %[16]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[17]s @user1 user user1@email %[3]s %[4]s 0 seconds 9 - --------- -Messages --------- - -* %[18]s %[5]s @user1 %[17]s @user1 user (user1@email) message 0 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) message 1 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) delete message 1 Deleted %[26]s -* %[20]s %[7]s @user1 %[17]s @user1 user (user1@email) message 2 UpdatedNoMsgChange %[27]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) message 3 -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) delete message 3 Deleted %[9]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Deleted file FileDeleted %[9]s -* %[22]s %[10]s @user1 %[17]s @user1 user (user1@email) message 4 EditedOriginalMsg %[28]s %[23]s -* %[23]s %[11]s @user1 %[17]s @user1 user (user1@email) edited message 4 EditedNewMsg %[28]s -* %[25]s %[12]s @user1 %[17]s @user1 user (user1@email) edited message 6 EditedNewMsg %[29]s diff --git a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm4.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm4.tmpl deleted file mode 100644 index a52c309944f..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch1SummaryPerm4.tmpl +++ /dev/null @@ -1,34 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[13]s -* TeamName: %[14]s -* TeamDisplayName: %[15]s -* ChannelId: %[16]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[17]s @user1 user user1@email %[3]s %[4]s 0 seconds 9 - --------- -Messages --------- - -* %[18]s %[5]s @user1 %[17]s @user1 user (user1@email) message 0 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) message 1 -* %[19]s %[6]s @user1 %[17]s @user1 user (user1@email) delete message 1 Deleted %[26]s -* %[20]s %[7]s @user1 %[17]s @user1 user (user1@email) message 2 UpdatedNoMsgChange %[27]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) message 3 -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Uploaded file -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) delete message 3 Deleted %[9]s -* %[21]s %[8]s @user1 %[17]s @user1 user (user1@email) Deleted file FileDeleted %[9]s -* %[23]s %[11]s @user1 %[17]s @user1 user (user1@email) edited message 4 EditedNewMsg %[28]s -* %[22]s %[10]s @user1 %[17]s @user1 user (user1@email) message 4 EditedOriginalMsg %[28]s %[23]s -* %[25]s %[12]s @user1 %[17]s @user1 user (user1@email) edited message 6 EditedNewMsg %[29]s diff --git a/server/enterprise/message_export/testdata/grE2E3Batch2Perm1.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch2Perm1.tmpl deleted file mode 100644 index 69ae80f23e1..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch2Perm1.tmpl +++ /dev/null @@ -1,63 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[10]s@user1useruser1@email%[3]s%[4]s0 seconds1
    - -

    Messages

    -
    - -
    - -

    Exported on %[15]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch2Perm2.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch2Perm2.tmpl deleted file mode 100644 index 5d263ed5be3..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch2Perm2.tmpl +++ /dev/null @@ -1,63 +0,0 @@ -

    Mattermost Compliance Export

    - -

    Conversation Summary

    -
    - -
    - - - - - - - - - - - - =20 - - - - - - - - - - - -
    UserId
    Username
    UserType
    EmailJoinedLeftDurationMessages
    %[10]s@user1useruser1@email%[3]s%[4]s0 seconds1
    - -

    Messages

    -
    - -
    - -

    Exported on %[15]s

    diff --git a/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm1.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm1.tmpl deleted file mode 100644 index 04b95a29da8..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm1.tmpl +++ /dev/null @@ -1,24 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[6]s -* TeamName: %[7]s -* TeamDisplayName: %[8]s -* ChannelId: %[9]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[10]s @user1 user user1@email %[3]s %[4]s 0 seconds 1 - --------- -Messages --------- - -* %[14]s %[5]s @user1 %[10]s @user1 user (user1@email) edited message 6 EditedNewMsg %[12]s diff --git a/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm2.tmpl b/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm2.tmpl deleted file mode 100644 index 3f5c304d1a7..00000000000 --- a/server/enterprise/message_export/testdata/grE2E3Batch2SummaryPerm2.tmpl +++ /dev/null @@ -1,24 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[6]s -* TeamName: %[7]s -* TeamDisplayName: %[8]s -* ChannelId: %[9]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[10]s @user1 user user1@email %[3]s %[4]s 0 seconds 1 - --------- -Messages --------- - -* %[13]s %[5]s @user1 %[10]s @user1 user (user1@email) message 6 EditedOriginalMsg %[12]s %[14]s diff --git a/server/enterprise/message_export/testdata/grE2E4Summary.tmpl b/server/enterprise/message_export/testdata/grE2E4Summary.tmpl deleted file mode 100644 index 5d7c87a12cb..00000000000 --- a/server/enterprise/message_export/testdata/grE2E4Summary.tmpl +++ /dev/null @@ -1,18 +0,0 @@ --------------------- -Conversation Summary --------------------- - -* TeamId: %[5]s -* TeamName: %[6]s -* TeamDisplayName: %[7]s -* ChannelId: %[8]s -* ChannelName: channel_two_name -* ChannelDisplayName: the Channel Two -* Started: %[1]s -* Ended: %[2]s -* Duration: 0 seconds - -UserId -Username -UserType -Email Joined Left Duration Messages %[9]s @user1 user user1@email %[3]s %[4]s 0 seconds 5 diff --git a/server/enterprise/message_export/worker.go b/server/enterprise/message_export/worker.go deleted file mode 100644 index b81f410304a..00000000000 --- a/server/enterprise/message_export/worker.go +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "context" - "errors" - "fmt" - "net/http" - "path" - "strconv" - "sync" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/jobs" - "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" - "github.com/mattermost/mattermost/server/v8/platform/shared/templates" -) - -const ( - TimeBetweenBatchesMs = 100 - DefaultPreviousJobPageSize = 5 -) - -// testEndOfBatchCb is only used for testing -var testEndOfBatchCb func(worker *MessageExportWorker) - -type MessageExportWorker struct { - name string - // stateMut protects stopCh, cancel, and stopped and helps enforce - // ordering in case subsequent Run or Stop calls are made. - stateMut sync.Mutex - stopCh chan struct{} - stopped bool - stoppedCh chan struct{} - jobs chan model.Job - jobServer *jobs.JobServer - logger mlog.LoggerIFace - htmlTemplateWatcher *templates.Container - license func() *model.License - - context context.Context - cancel func() -} - -func (dr *MessageExportJobInterfaceImpl) MakeWorker() model.Worker { - const workerName = "MessageExportWorker" - logger := dr.Server.Jobs.Logger().With(mlog.String("worker_name", workerName)) - - templatesDir, ok := fileutils.FindDir("templates") - if !ok { - logger.Error("Failed to initialize HTMLTemplateWatcher, templates directory not found") - return nil - } - htmlTemplateWatcher, err := templates.New(templatesDir) - if err != nil { - logger.Error("Failed to initialize HTMLTemplateWatcher", mlog.Err(err)) - return nil - } - - ctx, cancel := context.WithCancel(context.Background()) - - return &MessageExportWorker{ - name: workerName, - stoppedCh: make(chan struct{}, 1), - jobs: make(chan model.Job), - jobServer: dr.Server.Jobs, - logger: logger, - htmlTemplateWatcher: htmlTemplateWatcher, - // It is not a best practice to store context inside a struct, - // however we need to cancel a SQL query during a job execution. - // There is no other good way. - context: ctx, - cancel: cancel, - license: dr.Server.License, - stopped: true, - } -} - -func (w *MessageExportWorker) IsEnabled(cfg *model.Config) bool { - return w.license() != nil && *w.license().Features.MessageExport && *cfg.MessageExportSettings.EnableExport -} - -func (w *MessageExportWorker) Run() { - w.stateMut.Lock() - // We have to re-assign the stop channel again, because - // it might happen that the job was restarted due to a config change. - if w.stopped { - w.stopped = false - w.stopCh = make(chan struct{}) - w.context, w.cancel = context.WithCancel(context.Background()) - } else { - w.stateMut.Unlock() - return - } - // Run is called from a separate goroutine and doesn't return. - // So we cannot Unlock in a defer clause. - w.stateMut.Unlock() - - w.logger.Debug("Worker Started") - - defer func() { - w.logger.Debug("Worker finished") - w.stoppedCh <- struct{}{} - }() - - for { - select { - case <-w.stopCh: - w.logger.Debug("Worker: Received stop signal") - return - case job := <-w.jobs: - w.DoJob(&job) - } - } -} - -func (w *MessageExportWorker) Stop() { - w.stateMut.Lock() - defer w.stateMut.Unlock() - - // Set to close, and if already closed before, then return. - if w.stopped { - return - } - w.stopped = true - - w.logger.Debug("Worker: Stopping") - w.cancel() - close(w.stopCh) - <-w.stoppedCh -} - -func (w *MessageExportWorker) JobChannel() chan<- model.Job { - return w.jobs -} - -func (w *MessageExportWorker) DoJob(job *model.Job) { - logger := w.logger.With(jobs.JobLoggerFields(job)...) - logger.Debug("Worker: Received a new candidate job.") - defer w.jobServer.HandleJobPanic(logger, job) - - var appErr *model.AppError - job, appErr = w.jobServer.ClaimJob(job) - if appErr != nil { - logger.Warn("Worker: Error occurred while trying to claim job", mlog.Err(appErr)) - return - } else if job == nil { - return - } - - var cancelContext request.CTX = request.EmptyContext(w.logger) - cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background()) - cancelWatcherChan := make(chan struct{}, 1) - cancelContext = cancelContext.WithContext(cancelCtx) - go w.jobServer.CancellationWatcher(cancelContext, job.Id, cancelWatcherChan) - defer cancelCancelWatcher() - - rctx := request.EmptyContext(logger).WithContext(w.context) - // if job data is missing, we'll do our best to recover - w.initJobData(rctx, logger, job, time.Now()) - data, err := extractJobData(logger, job.Data) - if err != nil { - // Error in conversion. Not much we can do about that. But it shouldn't happen, unless someone edited the db. - w.setJobError(logger, job, model.NewAppError("Job.DoJob", "ent.message_export.job_data_conversion.app_error", nil, "", http.StatusBadRequest).Wrap(err)) - return - } - - reportProgress := func(message string) { - logger.Debug(message) - // Don't fail because we couldn't update progress. - w.setJobProgressMessage(0, message, rctx.Logger(), job) - } - - jobParams := shared.BackendParams{ - Config: w.jobServer.Config(), - Store: shared.NewMessageExportStore(w.jobServer.Store), - HtmlTemplates: w.htmlTemplateWatcher, - } - jobParams.FileAttachmentBackend, err = shared.GetFileAttachmentBackend(rctx, w.jobServer.Config()) - if err != nil { - w.setJobError(logger, job, model.NewAppError("GetFileAttachmentBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)) - return - } - jobParams.ExportBackend, err = shared.GetExportBackend(rctx, w.jobServer.Config()) - if err != nil { - w.setJobError(logger, job, model.NewAppError("GetExportBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)) - return - } - - data, err = shared.GetInitialExportPeriodData(rctx, jobParams.Store, data, reportProgress) - if err != nil { - w.setJobError(logger, job, model.NewAppError("DoJob", "ent.message_export.calculate_channel_exports.app_error", nil, "", http.StatusInternalServerError).Wrap(err)) - return - } - job.Data[shared.JobDataTotalPostsExpected] = strconv.Itoa(data.TotalPostsExpected) - - for { - select { - case <-cancelWatcherChan: - logger.Debug("Worker: Job has been canceled via CancellationWatcher") - w.setJobCanceled(logger, job) - return - - case <-w.stopCh: - logger.Debug("Worker: Job has been canceled via Worker Stop. Setting the job back to pending") - w.SetJobPending(logger, job) - return - - case <-time.After(TimeBetweenBatchesMs * time.Millisecond): - logger.Debug("Starting batch export", mlog.Int("last_post_update_at", data.Cursor.LastPostUpdateAt)) - - _, data, err = RunBatch(rctx, data, jobParams) - if err != nil { - // We ignore error if the job was explicitly cancelled - if errors.Is(w.context.Err(), context.Canceled) { - logger.Debug("Worker: Job has been canceled via worker's context. Setting the job back to pending") - w.SetJobPending(logger, job) - } else { - w.setJobError(logger, job, model.NewAppError("DoJob", "ent.message_export.run_export.app_error", nil, "", http.StatusInternalServerError).Wrap(err)) - } - return - } - - setJobDataEndOfBatch(job, data) - - if data.Finished { - w.finishExport(rctx, logger, job, data.WarningCount) - return - } - - // also saves job.Data - if err := w.setJobProgress(logger, job, getJobProgress(data.MessagesExported, data.TotalPostsExpected)); err != nil { - // TODO: MM-59093 handle job errors (robust, recoverable) - return - } - - // testEndOfBatchCb is only used by tests. - if testEndOfBatchCb != nil { - testEndOfBatchCb(w) - } - } - } -} - -func (w *MessageExportWorker) finishExport(rctx request.CTX, logger *mlog.Logger, job *model.Job, totalWarningCount int) { - job.Data[shared.JobDataWarningCount] = strconv.Itoa(totalWarningCount) - // we've exported everything up to the current time - logger.Debug("FormatExport complete") - - job.Data[shared.JobDataIsDownloadable] = "true" - - if totalWarningCount > 0 { - w.setJobWarning(logger, job) - } else { - w.setJobSuccess(logger, job) - } -} - -// initializes job data if it's missing, allows us to recover from failed or improperly configured jobs -func (w *MessageExportWorker) initJobData(rctx request.CTX, logger mlog.LoggerIFace, job *model.Job, now time.Time) { - if job.Data == nil { - job.Data = make(map[string]string) - } - if _, exists := job.Data[shared.JobDataMessagesExported]; !exists { - logger.Info("Worker: JobDataMessagesExported does not exist, starting at 0") - job.Data[shared.JobDataMessagesExported] = "0" - } - if _, exists := job.Data[shared.JobDataExportType]; !exists { - exportFormat := *w.jobServer.Config().MessageExportSettings.ExportFormat - logger.Info("Worker: Defaulting to configured export format", mlog.String("export_format", exportFormat)) - job.Data[shared.JobDataExportType] = exportFormat - } - if _, exists := job.Data[shared.JobDataBatchSize]; !exists { - batchSize := strconv.Itoa(*w.jobServer.Config().MessageExportSettings.BatchSize) - logger.Info("Worker: Defaulting to configured batch size", mlog.String("batch_size", batchSize)) - job.Data[shared.JobDataBatchSize] = batchSize - } - if _, exists := job.Data[shared.JobDataChannelBatchSize]; !exists { - channelBatchSize := strconv.Itoa(*w.jobServer.Config().MessageExportSettings.ChannelBatchSize) - logger.Info("Worker: Defaulting to configured channel batch size", mlog.String("channel_batch_size", channelBatchSize)) - job.Data[shared.JobDataChannelBatchSize] = channelBatchSize - } - if _, exists := job.Data[shared.JobDataChannelHistoryBatchSize]; !exists { - channelHistoryBatchSize := strconv.Itoa(*w.jobServer.Config().MessageExportSettings.ChannelHistoryBatchSize) - logger.Info("Worker: Defaulting to configured channel history batch size", mlog.String("channel_history_batch_size", channelHistoryBatchSize)) - job.Data[shared.JobDataChannelHistoryBatchSize] = channelHistoryBatchSize - } - if _, exists := job.Data[shared.JobDataBatchNumber]; !exists { - logger.Info("Worker: JobDataBatchNumber does not exist, starting at 0") - job.Data[shared.JobDataBatchNumber] = "0" - } - - // If this is a new job (JobEndTime doesn't exist), set it to now, because this is when the job has first started. - // The logic is that a job exports messages up to the moment the job was started. If the job was picked up after - // gracefully stopping, then run it until that original initial endTime. - // However, if the job was cancelled or errored out, that job will not be picked up again, so this will be a new job - // starting from the last successful batchStartTimestamp up until now. This is intentional (for now) because failed - // jobs do not get rescheduled properly yet, and when they are run again it means that new day's worth of messages - // need to be exported. - if _, exists := job.Data[shared.JobDataJobEndTime]; !exists { - millis := strconv.FormatInt(model.GetMillisForTime(now), 10) - logger.Info("Worker: JobDataJobEndTime not found in previous job, using now", mlog.String("job_data_job_end_time", millis)) - job.Data[shared.JobDataJobEndTime] = millis - } - - if _, exists := job.Data[shared.JobDataBatchStartTime]; !exists { - previousJob, err := w.getPreviousNonCliJob(rctx) - - if err != nil { - exportFromTimestamp := strconv.FormatInt(*w.jobServer.Config().MessageExportSettings.ExportFromTimestamp, 10) - logger.Info("Worker: No previously successful job found, falling back to configured MessageExportSettings.ExportFromTimestamp", mlog.String("export_from_timestamp", exportFromTimestamp)) - job.Data[shared.JobDataBatchStartTime] = exportFromTimestamp - job.Data[shared.JobDataJobStartTime] = exportFromTimestamp - job.Data[shared.JobDataBatchStartId] = "" - job.Data[shared.JobDataJobStartId] = job.Data[shared.JobDataBatchStartId] - job.Data[shared.JobDataExportDir] = getJobExportDir(logger, job.Data, exportFromTimestamp, job.Data[shared.JobDataJobEndTime]) - return - } - - logger.Info("Worker: Implicitly resuming export from where previously successful job left off") - if previousJob == nil { - previousJob = &model.Job{} - } - if previousJob.Data == nil { - previousJob.Data = make(map[string]string) - } - - // Backwards compatibility for <10.5: - if batchStartTimestamp, prevExists := previousJob.Data["batch_start_timestamp"]; prevExists { - previousJob.Data[shared.JobDataBatchStartTime] = batchStartTimestamp - } - - if _, prevExists := previousJob.Data[shared.JobDataBatchStartTime]; !prevExists { - exportFromTimestamp := strconv.FormatInt(*w.jobServer.Config().MessageExportSettings.ExportFromTimestamp, 10) - logger.Info("Worker: Previously successful job lacks job data, falling back to configured MessageExportSettings.ExportFromTimestamp", mlog.String("export_from_timestamp", exportFromTimestamp)) - job.Data[shared.JobDataBatchStartTime] = exportFromTimestamp - job.Data[shared.JobDataJobStartTime] = exportFromTimestamp - } else { - job.Data[shared.JobDataBatchStartTime] = previousJob.Data[shared.JobDataBatchStartTime] - } - if _, prevExists := previousJob.Data[shared.JobDataBatchStartId]; !prevExists { - logger.Info("Worker: Previously successful job lacks post ID, falling back to empty string") - job.Data[shared.JobDataBatchStartId] = "" - } else { - job.Data[shared.JobDataBatchStartId] = previousJob.Data[shared.JobDataBatchStartId] - } - job.Data[shared.JobDataJobStartId] = job.Data[shared.JobDataBatchStartId] - } else { - logger.Info("Worker: JobDataBatchStartTime start time was already set", - mlog.String(shared.JobDataBatchStartTime, job.Data[shared.JobDataBatchStartTime])) - } - - if _, exists := job.Data[shared.JobDataJobStartTime]; !exists { - // Just in case, if we don't have this (JobDataBatchStartTime was already set, but this wasn't) set it: - job.Data[shared.JobDataJobStartTime] = job.Data[shared.JobDataBatchStartTime] - logger.Info("Worker: JobDataJobStartTime start time was not set, using batch startTimestamp", - mlog.String(shared.JobDataJobStartTime, job.Data[shared.JobDataJobStartTime])) - } - - job.Data[shared.JobDataExportDir] = getJobExportDir(logger, job.Data, job.Data[shared.JobDataJobStartTime], job.Data[shared.JobDataJobEndTime]) -} - -// getPreviousNonCliJob returns the most recent job that was not initiated by mmctl -func (w *MessageExportWorker) getPreviousNonCliJob(rctx request.CTX) (*model.Job, error) { - offset := 0 - - for { - jobs, err := w.jobServer.Store.Job().GetAllByTypesAndStatusesPage(rctx, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - offset, DefaultPreviousJobPageSize) - if err != nil { - return nil, err - } - - // Find the first job not initiated by mmctl - for _, job := range jobs { - if job.Data == nil || job.Data[shared.JobDataInitiatedBy] != "mmctl" { - return job, nil - } - } - - // If we didn't get a full page of jobs, we've reached the end - if len(jobs) < DefaultPreviousJobPageSize { - return nil, nil - } - - // If we didn't find a non-mmctl job in this page, continue to the next page - offset += DefaultPreviousJobPageSize - } -} - -func extractJobData(logger *mlog.Logger, strmap map[string]string) (shared.JobData, error) { - data, err := shared.StringMapToJobDataWithZeroValues(strmap) - if err != nil { - return data, err - } - - // ExportPeriodStartTime is initialized to BatchStartTime because this is where we will start exporting. But unlike - // BatchStartTime, it won't change as we process the batches. - // If this is the first time this job has run, BatchStartTime will be the start of the entire job. If this job has - // been resumed, then BatchStartTime will be the start of the newest batch. This is expected--the channel activity - // and total posts will be calculated from ExportPeriodStartTime (anything earlier has already been exported in - // previous batches). - // Note: ExportPeriodStartTime is different from JobStartTime because JobStartTime won't change - // if the job processes some batches, is stopped, and picked up again. - data.ExportPeriodStartTime = data.BatchStartTime - - logger.Info("Worker: initial job variables set", - mlog.String("export_type", data.ExportType), - mlog.String("export_dir", data.ExportDir), - mlog.Int("job_start_time", data.JobStartTime), - mlog.Int("batch_start_time", data.BatchStartTime), - mlog.Int("export_period_start_time", data.ExportPeriodStartTime), - mlog.Int("job_end_time", data.JobEndTime), - mlog.String("job_start_id", data.JobStartId), - mlog.Int("batch_size", data.BatchSize), - mlog.Int("channel_batch_size", data.ChannelBatchSize), - mlog.Int("channel_history_batch_size", data.ChannelHistoryBatchSize), - mlog.Int("batch_number", data.BatchNumber), - mlog.Int("total_posts_exported", data.MessagesExported)) - - return data, err -} - -func setJobDataEndOfBatch(job *model.Job, data shared.JobData) { - job.Data[shared.JobDataBatchStartTime] = strconv.FormatInt(data.BatchStartTime, 10) - job.Data[shared.JobDataBatchStartId] = data.Cursor.LastPostId - job.Data[shared.JobDataMessagesExported] = strconv.Itoa(data.MessagesExported) - job.Data[shared.JobDataBatchNumber] = strconv.Itoa(data.BatchNumber) -} - -// getJobExportDir will use the existing JobDataExportDir if available. If it's not available, this is the first run -// for the job, so we use the startTime and endTime passed in. -func getJobExportDir(logger mlog.LoggerIFace, data model.StringMap, startTime string, endTime string) string { - exportDir, exists := data[shared.JobDataExportDir] - if !exists { - // If we don't have a jobDataExportDir, this is the first run for the job, so we use the batch startTime - exportDir = path.Join(model.ComplianceExportPath, fmt.Sprintf("%s-%s-%s", time.Now().Format(model.ComplianceExportDirectoryFormat), startTime, endTime)) - logger.Info("Worker: JobDataExportDir does not exist, using current datetime", mlog.String("job_data_export_dir", exportDir)) - } - - return exportDir -} - -func getJobProgress(totalExportedPosts, totalPostsExpected int) int { - return totalExportedPosts * 100 / totalPostsExpected -} - -func (w *MessageExportWorker) setJobProgressMessage(progress int64, message string, logger mlog.LoggerIFace, job *model.Job) { - job.Status = model.JobStatusInProgress - job.Progress = progress - if job.Data == nil { - job.Data = make(map[string]string) - } - job.Data["progress_message"] = message - - if _, err := w.jobServer.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err != nil { - logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) - } -} - -func (w *MessageExportWorker) setJobProgress(logger mlog.LoggerIFace, job *model.Job, progress int) error { - if job.Data != nil { - job.Data["progress_message"] = "" - } - - if err := w.jobServer.SetJobProgress(job, int64(progress)); err != nil { - logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) - w.setJobError(logger, job, err) - return err - } - - return nil -} - -func (w *MessageExportWorker) setJobSuccess(logger mlog.LoggerIFace, job *model.Job) { - // setting progress causes the job data to be saved, which is necessary if we want the next job to pick up where this one left off - if job.Data != nil { - job.Data["progress_message"] = "" - } - if err := w.jobServer.SetJobProgress(job, 100); err != nil { - logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) - w.setJobError(logger, job, err) - } - if err := w.jobServer.SetJobSuccess(job); err != nil { - logger.Error("Worker: Failed to set success for job", mlog.Err(err)) - w.setJobError(logger, job, err) - } -} - -func (w *MessageExportWorker) setJobWarning(logger mlog.LoggerIFace, job *model.Job) { - // setting progress causes the job data to be saved, which is necessary if we want the next job to pick up where this one left off - if job.Data != nil { - job.Data["progress_message"] = "" - } - if err := w.jobServer.SetJobProgress(job, 100); err != nil { - logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) - w.setJobError(logger, job, err) - } - if err := w.jobServer.SetJobWarning(job); err != nil { - logger.Error("Worker: Failed to set warning for job", mlog.Err(err)) - w.setJobError(logger, job, err) - } -} - -func (w *MessageExportWorker) setJobError(logger mlog.LoggerIFace, job *model.Job, appError *model.AppError) { - if job.Data != nil { - job.Data["progress_message"] = "" - } - logger.Error("Worker: Job error", mlog.Err(appError)) - if err := w.jobServer.SetJobError(job, appError); err != nil { - logger.Error("Worker: Failed to set job error", mlog.Err(err), mlog.NamedErr("set_error", appError)) - } -} - -func (w *MessageExportWorker) setJobCanceled(logger mlog.LoggerIFace, job *model.Job) { - if job.Data != nil { - job.Data["progress_message"] = "" - } - if err := w.jobServer.SetJobCanceled(job); err != nil { - logger.Error("Worker: Failed to mark job as canceled", mlog.Err(err)) - } -} - -func (w *MessageExportWorker) SetJobPending(logger mlog.LoggerIFace, job *model.Job) { - if job.Data != nil { - job.Data["progress_message"] = "" - } - if err := w.jobServer.SetJobPending(job); err != nil { - logger.Error("Worker: Failed to mark job as pending", mlog.Err(err)) - } -} diff --git a/server/enterprise/message_export/worker_test.go b/server/enterprise/message_export/worker_test.go deleted file mode 100644 index f64ec82b2a7..00000000000 --- a/server/enterprise/message_export/worker_test.go +++ /dev/null @@ -1,750 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.enterprise for license information. - -package message_export - -import ( - "context" - "errors" - "fmt" - "os" - "path" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - tmock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/jobs" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - st "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" - "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" - "github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared" -) - -func TestInitJobDataNoJobData(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - } - - // mock job store doesn't return a previously successful job, forcing fallback to config - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return(nil, errors.New("test")) - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - ConfigService: &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - }, - logger: logger, - } - - now := time.Now() - worker.initJobData(request.EmptyContext(logger), logger, job, now) - - assert.Equal(t, model.ComplianceExportTypeActiance, job.Data[shared.JobDataExportType]) - assert.Equal(t, strconv.Itoa(*worker.jobServer.Config().MessageExportSettings.BatchSize), job.Data[shared.JobDataBatchSize]) - assert.Equal(t, strconv.FormatInt(*worker.jobServer.Config().MessageExportSettings.ExportFromTimestamp, 10), job.Data[shared.JobDataBatchStartTime]) - expectedDir := path.Join(model.ComplianceExportPath, fmt.Sprintf("%s-%d-%d", now.Format(model.ComplianceExportDirectoryFormat), 0, now.UnixMilli())) - assert.Equal(t, expectedDir, job.Data[shared.JobDataExportDir]) -} - -func TestInitJobDataPreviousJobNoJobData(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - previousJob := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - StartAt: model.GetMillis() - 1000, - LastActivityAt: model.GetMillis() - 1000, - } - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - } - - // mock job store returns a previously successful job, but it doesn't have job data either, so we still fall back to config - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{previousJob}, nil) - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - ConfigService: &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - }, - logger: logger, - } - - now := time.Now() - worker.initJobData(request.EmptyContext(logger), logger, job, now) - - assert.Equal(t, model.ComplianceExportTypeActiance, job.Data[shared.JobDataExportType]) - assert.Equal(t, strconv.Itoa(*worker.jobServer.Config().MessageExportSettings.BatchSize), job.Data[shared.JobDataBatchSize]) - assert.Equal(t, strconv.FormatInt(*worker.jobServer.Config().MessageExportSettings.ExportFromTimestamp, 10), job.Data[shared.JobDataBatchStartTime]) - expectedDir := path.Join(model.ComplianceExportPath, fmt.Sprintf("%s-%d-%d", now.Format(model.ComplianceExportDirectoryFormat), 0, now.UnixMilli())) - assert.Equal(t, expectedDir, job.Data[shared.JobDataExportDir]) -} - -func TestInitJobDataPreviousJobWithJobData(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - previousJob := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - StartAt: model.GetMillis() - 1000, - LastActivityAt: model.GetMillis() - 1000, - Data: map[string]string{shared.JobDataBatchStartTime: "123"}, - } - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - Data: map[string]string{shared.JobDataExportDir: "this-is-the-export-dir"}, - } - - // mock job store returns a previously successful job that has the config that we're looking for, so we use it - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{previousJob}, nil) - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - ConfigService: &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - }, - logger: logger, - } - - now := time.Now() - worker.initJobData(request.EmptyContext(logger), logger, job, now) - - assert.Equal(t, model.ComplianceExportTypeActiance, job.Data[shared.JobDataExportType]) - assert.Equal(t, strconv.Itoa(*worker.jobServer.Config().MessageExportSettings.BatchSize), job.Data[shared.JobDataBatchSize]) - assert.Equal(t, previousJob.Data[shared.JobDataBatchStartTime], job.Data[shared.JobDataBatchStartTime]) - expectedDir := "this-is-the-export-dir" - assert.Equal(t, expectedDir, job.Data[shared.JobDataExportDir]) -} - -func TestInitJobDataPreviousJobWithJobDataPre105(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - previousJob := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - StartAt: model.GetMillis() - 1000, - LastActivityAt: model.GetMillis() - 1000, - Data: map[string]string{"batch_start_timestamp": "123"}, - } - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - Data: map[string]string{shared.JobDataExportDir: "this-is-the-export-dir"}, - } - - // mock job store returns a previously successful job that has the config that we're looking for, so we use it - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{previousJob}, nil) - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - ConfigService: &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - }, - logger: logger, - } - - now := time.Now() - worker.initJobData(request.EmptyContext(logger), logger, job, now) - - assert.Equal(t, model.ComplianceExportTypeActiance, job.Data[shared.JobDataExportType]) - assert.Equal(t, strconv.Itoa(*worker.jobServer.Config().MessageExportSettings.BatchSize), job.Data[shared.JobDataBatchSize]) - - // Assert the new job picks up the <10.5 job start time: - assert.Equal(t, previousJob.Data[shared.JobDataBatchStartTime], job.Data[shared.JobDataBatchStartTime]) - - expectedDir := "this-is-the-export-dir" - assert.Equal(t, expectedDir, job.Data[shared.JobDataExportDir]) -} - -func TestDoJobNoPostsToExport(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - mockMetrics := &mocks.MetricsInterface{} - defer mockMetrics.AssertExpectations(t) - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - } - retJob := *job - retJob.Status = model.JobStatusInProgress - - // claim job succeeds - mockStore.JobStore. - On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress). - Return(&retJob, nil) - mockMetrics.On("IncrementJobActive", model.JobTypeMessageExport) - - // no previous job, data will be loaded from config - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return(nil, errors.New("test")) - - // no channels with activity - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return(make([]string, 0), nil) - - // no posts found to export - mockStore.ComplianceStore.On("MessageExport", mock.Anything, mock.AnythingOfType("model.MessageExportCursor"), 10001).Return( - make([]*model.MessageExport, 0), model.MessageExportCursor{}, nil, - ) - - mockStore.PostStore.On("AnalyticsPostCount", mock.Anything).Return( - int64(shared.EstimatedPostCount), nil, - ) - - // job completed successfully - mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(true, nil) - mockStore.JobStore.On("UpdateStatus", job.Id, model.JobStatusSuccess).Return(job, nil) - mockMetrics.On("DecrementJobActive", model.JobTypeMessageExport) - - tempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(tempDir) - assert.NoError(t, err) - }) - - worker := &MessageExportWorker{ - jobServer: jobs.NewJobServer( - &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - FileSettings: model.FileSettings{ - DriverName: model.NewPointer(model.ImageDriverLocal), - Directory: model.NewPointer(tempDir), - }, - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - mockStore, - mockMetrics, - logger, - ), - logger: logger, - } - - // actually execute the code under test - worker.DoJob(job) -} - -func TestDoJobWithDedicatedExportBackend(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - mockMetrics := &mocks.MetricsInterface{} - defer mockMetrics.AssertExpectations(t) - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - } - retJob := *job - retJob.Status = model.JobStatusInProgress - - // claim job succeeds - mockStore.JobStore. - On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress). - Return(&retJob, nil) - mockMetrics.On("IncrementJobActive", model.JobTypeMessageExport) - - // no previous job, data will be loaded from config - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return(nil, errors.New("test")) - - channelId := st.NewTestID() - channelName := st.NewTestID() - channelDisplayName := st.NewTestID() - channelType := model.ChannelTypeOpen - messages := []*model.MessageExport{ - { - TeamId: model.NewPointer(st.NewTestID()), - ChannelId: model.NewPointer(channelId), - ChannelName: model.NewPointer(channelName), - UserId: model.NewPointer(st.NewTestID()), - UserEmail: model.NewPointer(st.NewTestID()), - Username: model.NewPointer(st.NewTestID()), - PostId: model.NewPointer(st.NewTestID()), - PostCreateAt: model.NewPointer[int64](123), - PostUpdateAt: model.NewPointer[int64](123), - PostDeleteAt: model.NewPointer[int64](123), - PostMessage: model.NewPointer(st.NewTestID()), - }, - } - - // need to export at least one post to make an export directory and file - - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return([]string{*messages[0].ChannelId}, nil) - mockStore.ChannelStore.On("GetMany", []string{channelId}, true). - Return(model.ChannelList{{ - Id: channelId, - DisplayName: channelDisplayName, - Name: channelName, - Type: channelType, - }}, nil) - - mockStore.ComplianceStore.On("MessageExport", mock.Anything, mock.AnythingOfType("model.MessageExportCursor"), 10001).Return( - messages, model.MessageExportCursor{}, nil, - ).Once() - mockStore.ComplianceStore.On("MessageExport", mock.Anything, mock.AnythingOfType("model.MessageExportCursor"), 10001).Return( - make([]*model.MessageExport, 0), model.MessageExportCursor{}, nil, - ).Once() - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - - mockStore.PostStore.On("AnalyticsPostCount", mock.Anything).Return( - int64(1), nil, - ) - - // job completed successfully - mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(true, nil) - mockStore.JobStore.On("UpdateStatus", job.Id, model.JobStatusSuccess).Return(job, nil) - mockMetrics.On("DecrementJobActive", model.JobTypeMessageExport) - - // create primary filestore directory - tempPrimaryDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - defer os.RemoveAll(tempPrimaryDir) - - // create dedicated filestore directory - tempDedicatedDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - defer os.RemoveAll(tempDedicatedDir) - - // setup worker with primary and dedicated filestores. - worker := &MessageExportWorker{ - jobServer: jobs.NewJobServer( - &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - FileSettings: model.FileSettings{ - DriverName: model.NewPointer(model.ImageDriverLocal), - Directory: model.NewPointer(tempPrimaryDir), - DedicatedExportStore: model.NewPointer(true), - ExportDriverName: model.NewPointer(model.ImageDriverLocal), - ExportDirectory: model.NewPointer(tempDedicatedDir), - }, - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - mockStore, - mockMetrics, - logger, - ), - logger: logger, - } - - // actually execute the code under test - worker.DoJob(job) - - // ensure no primary filestore files exist - files, err := os.ReadDir(tempPrimaryDir) - require.NoError(t, err) - assert.Zero(t, len(files)) - - // ensure some dedicated filestore files exist - files, err = os.ReadDir(tempDedicatedDir) - require.NoError(t, err) - assert.NotZero(t, len(files)) -} - -func TestDoJobCancel(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - - mockStore := &storetest.Store{} - t.Cleanup(func() { mockStore.AssertExpectations(t) }) - mockMetrics := &mocks.MetricsInterface{} - t.Cleanup(func() { mockMetrics.AssertExpectations(t) }) - - job := &model.Job{ - Id: st.NewTestID(), - CreateAt: model.GetMillis(), - Status: model.JobStatusPending, - Type: model.JobTypeMessageExport, - } - - tempDir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tempDir) }) - - impl := MessageExportJobInterfaceImpl{ - Server: &app.Server{ - Jobs: jobs.NewJobServer( - &testutils.StaticConfigService{ - Cfg: &model.Config{ - // mock config - FileSettings: model.FileSettings{ - DriverName: model.NewPointer(model.ImageDriverLocal), - Directory: model.NewPointer(tempDir), - }, - MessageExportSettings: model.MessageExportSettings{ - EnableExport: model.NewPointer(true), - ExportFormat: model.NewPointer(model.ComplianceExportTypeActiance), - DailyRunTime: model.NewPointer("01:00"), - ExportFromTimestamp: model.NewPointer(int64(0)), - BatchSize: model.NewPointer(10000), - ChannelBatchSize: model.NewPointer(100), - ChannelHistoryBatchSize: model.NewPointer(100), - }, - }, - }, - mockStore, - mockMetrics, - logger, - ), - }, - } - worker, ok := impl.MakeWorker().(*MessageExportWorker) - require.True(t, ok) - - retJob := *job - retJob.Status = model.JobStatusInProgress - - // Claim job succeeds - mockStore.JobStore. - On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress). - Return(&retJob, nil) - mockMetrics.On("IncrementJobActive", model.JobTypeMessageExport) - - // No previous job, data will be loaded from config - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return(nil, errors.New("test")) - - // Job updates the system console UI, once for getting channels, once for getting activity - mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(true, nil).Times(2) - - // a few calls pass - mockStore.ChannelMemberHistoryStore.On("GetChannelsWithActivityDuring", mock.Anything, mock.Anything). - Return([]string{"channel-id"}, nil) - mockStore.ChannelStore.On("GetMany", []string{"channel-id"}, true). - Return(model.ChannelList{{ - Id: "channel-id", - DisplayName: "channel-display-name", - Name: "channel-name", - Type: model.ChannelTypeDirect, - }}, nil) - mockStore.ChannelMemberHistoryStore.On("GetUsersInChannelDuring", mock.Anything, mock.Anything, []string{"channel-id"}).Return([]*model.ChannelMemberHistoryResult{}, nil) - - cancelled := make(chan struct{}) - // Cancel the worker and return an error - mockStore.ComplianceStore.On("MessageExport", mock.Anything, mock.AnythingOfType("model.MessageExportCursor"), 10001).Run(func(args tmock.Arguments) { - worker.cancel() - - rctx, ok := args.Get(0).(request.CTX) - require.True(t, ok) - assert.Error(t, rctx.Context().Err()) - assert.ErrorIs(t, rctx.Context().Err(), context.Canceled) - - cancelled <- struct{}{} - }).Return( - nil, model.MessageExportCursor{}, context.Canceled, - ) - - mockStore.PostStore.On("AnalyticsPostCount", mock.Anything).Return( - int64(shared.EstimatedPostCount), nil, - ) - - // Job marked as pending - mockStore.JobStore.On("UpdateStatus", job.Id, model.JobStatusPending).Return(job, nil) - mockMetrics.On("DecrementJobActive", model.JobTypeMessageExport) - - go worker.Run() - - worker.JobChannel() <- *job - - // Wait for the cancelation - <-cancelled - - // Cleanup - worker.Stop() -} - -func TestGetPreviousJobNoJobs(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - // Mock the job store to return empty jobs list - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{}, nil).Once() - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - }, - logger: logger, - } - - rctx := request.EmptyContext(logger) - job, err := worker.getPreviousNonCliJob(rctx) - - require.NoError(t, err) - assert.Nil(t, job, "Expected nil job when no jobs are returned") -} - -func TestGetPreviousJobOneRegularJob(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - regularJob := &model.Job{ - Id: st.NewTestID(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - Data: map[string]string{}, - } - - // Mock the job store to return one regular job - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{regularJob}, nil).Once() - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - }, - logger: logger, - } - - rctx := request.EmptyContext(logger) - job, err := worker.getPreviousNonCliJob(rctx) - - require.NoError(t, err) - assert.Equal(t, regularJob.Id, job.Id, "Expected to get the regular job") -} - -func TestGetPreviousJobOneMmctlJob(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - mmctlJob := &model.Job{ - Id: st.NewTestID(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - Data: map[string]string{shared.JobDataInitiatedBy: "mmctl"}, - } - - // Mock the job store to return only mmctl jobs (4 jobs, not a full page) - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return([]*model.Job{mmctlJob, mmctlJob, mmctlJob, mmctlJob}, nil).Once() - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - }, - logger: logger, - } - - rctx := request.EmptyContext(logger) - job, err := worker.getPreviousNonCliJob(rctx) - - require.NoError(t, err) - assert.Nil(t, job, "Expected nil job when only mmctl jobs are found") -} - -func TestGetPreviousJobManyJobs(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(t) - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - // Create DefaultPageSize mmctl jobs for first page - firstPageJobs := make([]*model.Job, DefaultPreviousJobPageSize) - for i := range DefaultPreviousJobPageSize { - firstPageJobs[i] = &model.Job{ - Id: st.NewTestID(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - Data: map[string]string{shared.JobDataInitiatedBy: "mmctl"}, - } - } - - // Create DefaultPageSize mmctl jobs for second page - secondPageJobs := make([]*model.Job, DefaultPreviousJobPageSize) - for i := range DefaultPreviousJobPageSize { - secondPageJobs[i] = &model.Job{ - Id: st.NewTestID(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - Data: map[string]string{shared.JobDataInitiatedBy: "mmctl"}, - } - } - - // Create 1 regular job for the third page (last job) - regularJob := &model.Job{ - Id: st.NewTestID(), - Status: model.JobStatusSuccess, - Type: model.JobTypeMessageExport, - Data: map[string]string{}, - } - thirdPageJobs := []*model.Job{regularJob} - - // Mock the job store to return the jobs in pages - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 0, DefaultPreviousJobPageSize).Return(firstPageJobs, nil).Once() - - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 1*DefaultPreviousJobPageSize, DefaultPreviousJobPageSize).Return(secondPageJobs, nil).Once() - - mockStore.JobStore.On("GetAllByTypesAndStatusesPage", mock.Anything, - []string{model.JobTypeMessageExport}, - []string{model.JobStatusWarning, model.JobStatusSuccess}, - 2*DefaultPreviousJobPageSize, DefaultPreviousJobPageSize).Return(thirdPageJobs, nil).Once() - - worker := &MessageExportWorker{ - jobServer: &jobs.JobServer{ - Store: mockStore, - }, - logger: logger, - } - - rctx := request.EmptyContext(logger) - job, err := worker.getPreviousNonCliJob(rctx) - - require.NoError(t, err) - assert.NotNil(t, job) - assert.Equal(t, regularJob.Id, job.Id, "Expected to find the regular job at the end") -}