Files
team/server/enterprise/message_export/message_export_test.go
77181c73e2 MM-59966 - Compliance Export overhaul - feature branch (#29789)
* [MM-59089] Add a compliance export constant (#27919)

* add a useful constant

* i18n

* another constant

* another i18n

* [MM-60422] Add GetChannelsWithActivityDuring (#28301)

* modify GetUsersInChannelDuring to accept a slice of channelIds

* add GetChannelsWithActivityDuring

* add compliance export progress message; remove unused custom status

* linting

* tests running too fast

* add batch size config settings

* add store tests

* linting

* empty commit

* i18n changes

* fix i18n ordering

* MM-60570 - Server-side changes consolidating the export CLI with server/ent code (#28640)

* add an i18n field; add the CLI's export directory

* int64 -> int

* Add UntilUpdateAt for MessageExport and AnalyticsPostCount

to merge

* remove now-unused i18n strings

* add TranslationsPreInitFromBuffer to allow CLI to use i18n

* use GetBuilder to simplify; rename TranslationsPreInitFromFileBytes

* [MM-59089] Improve compliance export timings (#1733 - Enterprise repo)

* MM-60422 - Performance and logic fixes for Compliance Exports (#1757 - Enterprise repo)

* MM-60570 - Enterprise-side changes consolidating the export CLI with server/ent code (#1769 - Enterprise repo)

* merge conflicts; missed file from ent branch

* MM-61038 - Add an option to sqlstore.New (#28702)

remove useless comment

add test

add an option to sqlstore.New

* MM-60976: Remove RunExport command from Mattermost binary (#28805)

* remove RunExport command from mattermost binary

* remove the code it was calling

* fix i18n

* remove test (was only testing license, not functionality)

* empty commit

* fix flaky GetChannelsWithActivityDuring test

* MM-60063: Dedicated Export Filestore fix, redo of #1772 (enterprise) (#28803)

* redo filestore fix #1772 (enterprise repo) on top of MM-59966 feature

* add new e2e tests for export filestore

* golint

* ok, note to self: shadowing bad, actually (when there's a defer)

* empty commit

* MM-61137 - Message export: Support 7.8.11 era dbs (#28824)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* blank commit

* MM-60974 - Message Export: Add performance metrics (#28836)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* cleanup unneeded named returns

* blank commit

* MM-60975 - Message export: Add startTime and endTime to export folder name (#28840)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* output startTime and endTime in export folder

* empty commit

* merge conflict

* MM-60978 - Message export: Improve xml fields; fix delete semantics (#28873)

* support 7.8.11 era dbs by wrapping the store using only what we need

* fix flaky GetChannelsWithActivityDuring test

* add a comment

* only need to define the MEFileInfoStore (the one that'll be overridden)

* performance metrics

* output startTime and endTime in export folder

* empty commit

* add xml fields, omit when empty, tests

* fix delete semantics; test (and test for update semantics)

* clarify comments

* simplify edited post detection, now there's no edge case.

* add some spacing to help fast running tests

* merge conflicts/updates needed for new deleted post semantics

* linting; fixing tests from upstream merge

* use SafeDereference

* linting

* stronger typing; better wrapped errors; better formatting

* blank commit

* goimports formatting

* fix merge mistake

* minor fixes due to changes in master

* MM-61755 - Simplifying and Support reporting to the db from the CLI (#29281)

* finally clean up JobData struct and stringMap; prep for CLI using db

* and now simplify using StringMapToJobDataWithZeroValues

* remove unused fn

* create JobDataExported; clean up errors

* MM-60176 - Message Export: Global relay cleanup (#29168)

* move global relay logic into global_relay_export

* blank commit

* blank commit

* improve errors

* MM-60693 - Refactor CSV to use same codepath as Actiance (#29191)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* MM-61696 - Refactor GlobalRelay to use same codepath as Actiance (#29225)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* remove newly unneeded function and its test. goodbye.

* refactor GetPostAttachments for csv + global relay to share

* refactor global_relay_export and fix tests (no changes to output)

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* remove unneeded nil check

* PR comments

* MM-61715 - Generalize e2e to all export types 🤖  (#29369)

* move global relay logic into global_relay_export

* blank commit

* refactor (and simplify) ExportParams into shared

* blank commit

* remove unused fn

* csv now uses pre-calculated joins/leaves like actiance

* remove newly unneeded function and its test. goodbye.

* refactor GetPostAttachments for csv + global relay to share

* refactor global_relay_export and fix tests (no changes to output)

* improve errors

* remove nil post check; remove ignoredPosts metric

* remove unneeded copy

* remove unneeded nil check

* PR comments

* refactor isDeletedMsg for all export types

* fix start and endtime, nasty csv createAt bug; bring closer to Actiance

* align unit tests with new logic (e.g. starttime / endtime)

* refactor a TimestampConvert fn for code + tests

* bug: pass templates to global relay (hurray for e2e tests, otherwise...)

* add global relay zip to allowed list (only for tests)

* test helpers

* new templates for e2e tests

* e2e tests... phew.

* linting

* merge conflicts

* unexport PostToRow; add test helper marker

* cleanup, shortening, thanks to PR comments

* MM-61972 - Generalize export data path - Actiance (#29399)

* extract and generalize the export data generation functions

* finish moving test (bc of previous extraction)

* lift a function from common -> shared (to break an import cycle)

* actiance now takes general export data, processes it into actiance data

* bring tests in line with correct sorting rules (upadateAt, messageId)

* fixups, PR comments

* turn strings.Repeat into a more descriptive const

amended: one letter fix; bad rebase

* MM-62009 - e2e clock heisenbug (#29434)

* consolidate assertions; output debuggable diffs (keeping for future)

* refactor test output generator to generators file

* waitUntilZeroPosts + pass through until to job = fix all clock issues

* simplify messages to model.NewId(); remove unneeded waitUntilZeroPosts

* model.NewId() -> storetest.NewTestID()

* MM-61980 - Generalize export data path - CSV (#29482)

* simple refactoring

* increase sleep times for (very) rare test failures

* add extra information to the generic export for CSV

* adj Actiance to handle new generic export (no difference in its output)

* no longer need mergePosts (yay), move getJoinLeavePosts for everyone

* adjust tests for new csv semantics (detailed in summary)

* and need to add the new exported data to the export_data_tests

* rearrange csv writing to happen after data export (more logical)

* linting

* remove debug statements

* figured out what was wrong with global relay e2e test 3; solid now

* PR comments

* MM-61718 - Generalize export data path - Global Relay (#29508)

* move global relay over to using the generalized export data

* performance pass -- not much can be done

* Update server/enterprise/message_export/global_relay_export/global_relay_export.go

Co-authored-by: Claudio Costa <[email protected]>

---------

Co-authored-by: Claudio Costa <[email protected]>

* MM-62058 - Align CSV with Actiance (#29551)

* refactoring actiance files and var names for clarity

* bug found in exported attachments (we used to miss some start/ends)

* changes needed for actiance due to new generic exports

* bringing CSV up to actiance standards

* fixing global relay b/c of new semantics (adding a note on an edge case)

* aligning e2e tests, adding comments to clarify what is expected/tested

* necessary changes; 1 more test for added functionality (ignoreDeleted)

* comment style

* MM-62059 - Align Global Relay with Actiance/CSV; many fixes (#29665)

* core logic changes to general export_data and the specific export paths

* unit tests and e2e tests, covering all new edge cases and all logic

* linting

* better var naming, const value, and cleaning up functions calls

* MM-62436 - Temporarily skip cypress tests that require download link (#29772)

---------

Co-authored-by: Claudio Costa <[email protected]>
2025-01-10 16:56:02 -05:00

2613 lines
102 KiB
Go

// 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"
"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("[email protected]"),
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 i := 0; i < 3; i++ {
_, 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 := 0; i < 3; i++ {
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 := false
for _, perm := range expectedExports {
if export == perm {
matched = true
break
}
}
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 = false
for _, perm := range expectedExports {
if export == perm {
matched = true
break
}
}
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("<div class=3D\"summary-list\">\n <ul>\n <li><span class=3D\"bold\">TeamId:</span>%[1]s</li>\n <li><span class=3D\"bold\">TeamName:</span>%[2]s</li>\n <li><span class=3D\"bold\">TeamDisplayName:</span>%[3]s</li>\n <li><span class=3D\"bold\">ChannelId:</span>%[4]s</li>\n <li><span class=3D\"bold\">ChannelName:</span>channel_two_name</li>\n <li><span class=3D\"bold\">ChannelDisplayName:</span>the Channel Two</li>\n <li><span class=3D\"bold\">Started:</span>%[5]s</li>\n <li><span class=3D\"bold\">Ended:</span>%[6]s</li>\n <li><span class=3D\"bold\">Duration:</span>0 seconds</li>\n </ul>\n</div>\n",
teams[0].Id, teams[0].Name, teams[0].DisplayName, channels[0].Id, conv(batchStartTime), conv(batchEndTime)),
fmt.Sprintf("<td class=3D\"userid\">%[1]s</td>\n <td class=3D\"username\">@user1</td>\n <td class=3D\"usertype\">user</td>\n <td class=3D\"email\">user1@email</td>\n <td class=3D\"joined\">%[2]s</td>\n <td class=3D\"left\">%[3]s</td>\n <td class=3D\"duration\">0 seconds</td>\n <td class=3D\"messages\">5</td>",
users[0].Id, conv(jl[0].join), conv(batchEndTime)),
fmt.Sprintf("<span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@user1</span>\n <span class=3D\"userid\">%[3]s</span>\n <span class=3D\"postusername\">@user1</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(user1@email)</span>\n <span class=3D\"message\">edited message 0</span>\n <span class=3D\"update_type\">EditedNewMsg</span>\n <span class=3D\"update_time\">%[4]s</span>\n <span class=3D\"edited_new_msg_id\"></span>",
posts[1].Id, conv(posts[1].CreateAt), users[0].Id, conv(posts[1].UpdateAt)),
fmt.Sprintf("<span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@user1</span>\n <span class=3D\"userid\">%[3]s</span>\n <span class=3D\"postusername\">@user1</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(user1@email)</span>\n <span class=3D\"message\">message 0</span>\n <span class=3D\"update_type\">EditedOriginalMsg</span>\n <span class=3D\"update_time\">%[4]s</span>\n <span class=3D\"edited_new_msg_id\">%[5]s</span>",
posts[0].Id, conv(posts[0].CreateAt), users[0].Id, conv(posts[0].UpdateAt), posts[1].Id),
fmt.Sprintf("<span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@user1</span>\n <span class=3D\"userid\">%[3]s</span>\n <span class=3D\"postusername\">@user1</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(user1@email)</span>\n <span class=3D\"message\">message 2</span>",
posts[2].Id, conv(posts[2].CreateAt), users[0].Id, conv(posts[2].UpdateAt)),
fmt.Sprintf("<span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@user1</span>\n <span class=3D\"userid\">%[3]s</span>\n <span class=3D\"postusername\">@user1</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(user1@email)</span>\n <span class=3D\"message\">message 3</span>",
posts[3].Id, conv(posts[3].CreateAt), users[0].Id, conv(posts[3].UpdateAt)),
fmt.Sprintf("<span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@user1</span>\n <span class=3D\"userid\">%[3]s</span>\n <span class=3D\"postusername\">@user1</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(user1@email)</span>\n <span class=3D\"message\">message 4</span>",
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 := "<li class=3D\"message\">\n <span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@%[3]s</span>\n <span class=3D\"userid\">%[4]s</span>\n <span class=3D\"postusername\">@%[3]s</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(%[5]s)</span>\n <span class=3D\"message\">%[6]s</span>\n</li>"
// 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 := "<li class=3D\"message\">\n <span class=3D\"post_id\">%[1]s</span>\n <span class=3D\"sent_time\">%[2]s</span>\n <span class=3D\"username\">@%[3]s</span>\n <span class=3D\"userid\">%[4]s</span>\n <span class=3D\"postusername\">@%[3]s</span>\n <span class=3D\"usertype\">user</span>\n <span class=3D\"email\">(%[5]s)</span>\n <span class=3D\"message\">%[6]s</span>\n <span class=3D\"update_type\">%[7]s</span>\n <span class=3D\"update_time\">%[8]s</span>\n <span class=3D\"edited_new_msg_id\">%[9]s</span>\n</li>\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.Split(msg, "\n") {
if strings.Contains(l, "<li class=3D\"message\">") {
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 := 0; i < 10; i++ {
_, 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
}