// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "fmt" "strconv" "testing" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/stretchr/testify/require" ) type MockReportable struct { TestField1 string TestField2 int TestField3 time.Time } func (mr *MockReportable) ToReport() []string { return []string{ mr.TestField1, strconv.Itoa(mr.TestField2), mr.TestField3.Format("2006-01-02"), } } var testData []model.ReportableObject = []model.ReportableObject{ &MockReportable{ TestField1: "some-name", TestField2: 400, TestField3: time.Date(2024, 1, 1, 0, 0, 0, 0, time.Local), }, &MockReportable{ TestField1: "some-other-name", TestField2: 500, TestField3: time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local), }, &MockReportable{ TestField1: "some-other-other-name", TestField2: 600, TestField3: time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local), }, } func TestSaveReportChunk(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) t.Run("should write CSV chunk to file", func(t *testing.T) { prefix := model.NewId() err := th.App.SaveReportChunk("csv", prefix, 999, []model.ReportableObject{testData[0]}) require.Nil(t, err) filePath := fmt.Sprintf("admin_reports/batch_report_%s__999.csv", prefix) bytes, err := th.App.ReadFile(filePath) require.Nil(t, err) require.NotNil(t, bytes) require.Equal(t, "some-name,400,2024-01-01\n", string(bytes)) }) t.Run("should fail if the report format is not supported", func(t *testing.T) { err := th.App.SaveReportChunk("zzz", model.NewId(), 999, []model.ReportableObject{testData[0]}) require.NotNil(t, err) }) } func TestCompileReportChunks(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) prefix := model.NewId() err := th.App.SaveReportChunk("csv", prefix, 0, []model.ReportableObject{testData[0]}) require.Nil(t, err) err = th.App.SaveReportChunk("csv", prefix, 1, []model.ReportableObject{testData[1]}) require.Nil(t, err) err = th.App.SaveReportChunk("csv", prefix, 2, []model.ReportableObject{testData[2]}) require.Nil(t, err) t.Run("should compile a bunch of report chunks", func(t *testing.T) { compileErr := th.App.CompileReportChunks("csv", prefix, 3, []string{"Name", "NumPosts", "StartDate"}) require.Nil(t, compileErr) filePath := fmt.Sprintf("admin_reports/batch_report_%s.csv", prefix) bytes, readErr := th.App.ReadFile(filePath) require.Nil(t, readErr) require.NotNil(t, bytes) expected := `Name,NumPosts,StartDate some-name,400,2024-01-01 some-other-name,500,2023-01-01 some-other-other-name,600,2022-01-01 ` require.Equal(t, expected, string(bytes)) }) t.Run("should fail if the report format is not supported", func(t *testing.T) { err = th.App.CompileReportChunks("zzz", prefix, 3, []string{"Name", "NumPosts", "StartDate"}) require.NotNil(t, err) }) t.Run("should fail if a chunk is missing", func(t *testing.T) { err = th.App.CompileReportChunks("csv", prefix, 4, []string{"Name", "NumPosts", "StartDate"}) require.NotNil(t, err) }) } func TestCheckForExistingJobs(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) t.Run("should return error if job with same options exists in pending jobs", func(t *testing.T) { app := th.App options := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "user", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", } jobType := model.JobTypeExportUsersToCSV // Create a pending job with same options job, err := app.Srv().Jobs.CreateJob(th.Context, jobType, options) defer func() { _ = app.Srv().Jobs.RequestCancellation(th.Context, job.Id) }() require.Nil(t, err) require.NotNil(t, job) // checkForExistingJobs appErr := app.checkForExistingJobs(th.Context, options, jobType) require.NotNil(t, appErr) require.Equal(t, "app.report.start_users_batch_export.job_exists", appErr.Id) }) t.Run("should return error if job with same options exists in in-progress jobs", func(t *testing.T) { app := th.App options := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "user", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", } jobType := model.JobTypeExportUsersToCSV // Create an in-progress job with same options job, err := app.Srv().Jobs.CreateJob(th.Context, jobType, options) defer func() { _ = app.Srv().Jobs.RequestCancellation(th.Context, job.Id) }() require.Nil(t, err) require.NotNil(t, job) // Manually set job status to in-progress err = app.Srv().Jobs.SetJobProgress(job, 60) require.Nil(t, err) // Call checkForExistingJobs appErr := app.checkForExistingJobs(th.Context, options, jobType) require.NotNil(t, appErr) require.Equal(t, "app.report.start_users_batch_export.job_exists", appErr.Id) }) t.Run("should not return error if existing jobs have different options", func(t *testing.T) { app := th.App options := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "user", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", } jobType := model.JobTypeExportUsersToCSV differentOptions := map[string]string{ "date_range": "all_time", "requesting_user_id": th.BasicUser2.Id, "role": "admin", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", } job, err := app.Srv().Jobs.CreateJob(th.Context, jobType, differentOptions) require.Nil(t, err) require.NotNil(t, job) // Call checkForExistingJobs appErr := app.checkForExistingJobs(th.Context, options, jobType) require.Nil(t, appErr) }) t.Run("should not return error if existing job has different search_term", func(t *testing.T) { app := th.App options := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", "search_term": "alice", } jobType := model.JobTypeExportUsersToCSV existingJobOptions := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "", "search_term": "bob", } job, err := app.Srv().Jobs.CreateJob(th.Context, jobType, existingJobOptions) require.Nil(t, err) require.NotNil(t, job) appErr := app.checkForExistingJobs(th.Context, options, jobType) require.Nil(t, appErr) }) t.Run("should not return error if existing job has different guest_filter", func(t *testing.T) { app := th.App options := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "single_channel", } jobType := model.JobTypeExportUsersToCSV existingJobOptions := map[string]string{ "date_range": "last_30_days", "requesting_user_id": th.BasicUser.Id, "role": "", "team": "", "hide_active": "false", "hide_inactive": "false", "guest_filter": "multi_channel", } job, err := app.Srv().Jobs.CreateJob(th.Context, jobType, existingJobOptions) require.Nil(t, err) require.NotNil(t, job) appErr := app.checkForExistingJobs(th.Context, options, jobType) require.Nil(t, appErr) }) } func TestStartUsersBatchExport(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) rctx := th.Context.WithSession(&model.Session{UserId: th.BasicUser.Id}) startExport := func(searchTerm string) *model.AppError { return th.App.StartUsersBatchExport(rctx, &model.UserReportOptions{ ReportingBaseOptions: model.ReportingBaseOptions{DateRange: model.ReportDurationAllTime}, SearchTerm: searchTerm, }, 0, model.GetMillis()) } searchTermsOfPendingExports := func(t *testing.T) []string { t.Helper() jobs, appErr := th.App.Srv().Jobs.GetJobsByTypeAndStatus(rctx, model.JobTypeExportUsersToCSV, model.JobStatusPending) require.Nil(t, appErr) var searchTerms []string for _, job := range jobs { if job.Data["requesting_user_id"] == th.BasicUser.Id { searchTerms = append(searchTerms, job.Data["search_term"]) } } return searchTerms } t.Run("should record the search term on the export job", func(t *testing.T) { require.Nil(t, startExport("term-recorded")) require.Contains(t, searchTermsOfPendingExports(t), "term-recorded") }) t.Run("should queue a separate job when only the search term differs", func(t *testing.T) { require.Nil(t, startExport("term-first")) require.Nil(t, startExport("term-second")) require.Subset(t, searchTermsOfPendingExports(t), []string{"term-first", "term-second"}) }) t.Run("should still reject a duplicate export of the same search term", func(t *testing.T) { require.Nil(t, startExport("term-repeated")) appErr := startExport("term-repeated") require.NotNil(t, appErr) require.Equal(t, "app.report.start_users_batch_export.job_exists", appErr.Id) }) }