Files
upstream-mattermost/server/channels/jobs/extract_content/worker_test.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

259 lines
8.7 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package extract_content
import (
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
)
type trackingApp struct {
errOn map[string]error
calls []string
}
func (a *trackingApp) ExtractContentFromFileInfo(_ request.CTX, fileInfo *model.FileInfo) error {
a.calls = append(a.calls, fileInfo.Id)
if err, ok := a.errOn[fileInfo.Id]; ok {
return err
}
return nil
}
func makeTestJobServer(t *testing.T) (*jobs.JobServer, *storetest.Store) {
t.Helper()
mockStore := &storetest.Store{}
t.Cleanup(func() {
mockStore.AssertExpectations(t)
})
jobServer := jobs.NewJobServer(
&testutils.StaticConfigService{},
mockStore,
nil,
mlog.CreateConsoleTestLogger(t),
nil,
)
return jobServer, mockStore
}
func expectJobDataUpdate(mockStore *storetest.Store) {
mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(nil, nil)
}
func expectWorkerJobCompletion(mockStore *storetest.Store, job *model.Job) {
claimed := *job
claimed.Status = model.JobStatusInProgress
mockStore.JobStore.On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress).Return(&claimed, nil)
mockStore.JobStore.On("UpdateStatus", job.Id, model.JobStatusSuccess).Return(&claimed, nil)
}
func makeFileInfo(id string, createAt int64, ext string) *model.FileInfo {
return &model.FileInfo{
Id: id,
CreateAt: createAt,
Extension: ext,
Name: "file." + ext,
Path: "path/" + id,
}
}
func makeFileInfoBatch(n int, startCreateAt int64) []*model.FileInfo {
batch := make([]*model.FileInfo, n)
for i := range n {
batch[i] = makeFileInfo(model.NewId(), startCreateAt+int64(i), "pdf")
}
return batch
}
func TestRunCatchupExtraction(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("extracts non-ignored files and passes OnlyEmptyContent filter", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
pdf := makeFileInfo("pdf1", 1000, "pdf")
png := makeFileInfo("png1", 1001, "png")
docx := makeFileInfo("docx1", 1002, "docx")
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
return opt.OnlyEmptyContent && !opt.IncludeDeleted && opt.SortBy == model.FileinfoSortByCreated
})).Return([]*model.FileInfo{pdf, png, docx}, nil).Once()
app := &trackingApp{}
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
require.NoError(t, err)
require.Equal(t, []string{"pdf1", "docx1"}, app.calls)
require.Equal(t, "2", job.Data["processed"])
require.Equal(t, "0", job.Data["errors"])
})
t.Run("empty result is a no-op", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(nil, nil).Once()
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
require.NoError(t, err)
require.Equal(t, "0", job.Data["processed"])
require.Equal(t, "0", job.Data["errors"])
})
t.Run("full batch advances cursor and fetches the next page", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
first := makeFileInfoBatch(catchupBatchSize, 1000)
second := []*model.FileInfo{makeFileInfo("tail1", 2000, "pdf"), makeFileInfo("tail2", 2001, "pdf")}
var firstPageOpts *model.GetFileInfosOptions
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
return opt.OnlyEmptyContent
})).Run(func(args mock.Arguments) {
opt := args.Get(2).(*model.GetFileInfosOptions)
copied := *opt
firstPageOpts = &copied
}).Return(first, nil).Once()
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
return opt.Since == 1999+1
})).Return(second, nil).Once()
sinceLower := model.GetMillis() - catchupLookback.Milliseconds()
app := &trackingApp{}
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
sinceUpper := model.GetMillis() - catchupLookback.Milliseconds()
require.NoError(t, err)
require.NotNil(t, firstPageOpts)
require.GreaterOrEqual(t, firstPageOpts.Since, sinceLower)
require.LessOrEqual(t, firstPageOpts.Since, sinceUpper)
require.Len(t, app.calls, catchupBatchSize+2)
require.Equal(t, "1002", job.Data["processed"])
})
t.Run("partial batch terminates after one fetch", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
batch := []*model.FileInfo{
makeFileInfo("f1", 1000, "pdf"),
makeFileInfo("f2", 1001, "pdf"),
makeFileInfo("f3", 1002, "pdf"),
}
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(batch, nil).Once()
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
require.NoError(t, err)
require.Equal(t, "3", job.Data["processed"])
})
t.Run("accumulates extraction errors in job data", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
okFile := makeFileInfo("ok", 1000, "pdf")
failFile := makeFileInfo("fail", 1001, "pdf")
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).
Return([]*model.FileInfo{okFile, failFile}, nil).Once()
app := &trackingApp{errOn: map[string]error{"fail": errors.New("extract failed")}}
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, app, mockStore)
require.NoError(t, err)
require.Equal(t, "2", job.Data["processed"])
require.Equal(t, "1", job.Data["errors"])
})
t.Run("store error propagates", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
wantErr := errors.New("store failed")
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.Anything).Return(nil, wantErr).Once()
job := &model.Job{Data: map[string]string{}}
err := runCatchupExtraction(logger, job, jobServer, &trackingApp{}, mockStore)
require.ErrorIs(t, err, wantErr)
})
}
func TestWorkerDispatch(t *testing.T) {
t.Run("catchup job uses OnlyEmptyContent filter", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
return opt.OnlyEmptyContent
})).Return(nil, nil).Once()
worker := MakeWorker(jobServer, &trackingApp{}, mockStore)
job := &model.Job{Id: model.NewId(), Data: map[string]string{catchupJobDataKey: "true"}}
expectWorkerJobCompletion(mockStore, job)
worker.DoJob(job)
})
t.Run("range job does not use OnlyEmptyContent filter", func(t *testing.T) {
jobServer, mockStore := makeTestJobServer(t)
expectJobDataUpdate(mockStore)
mockStore.FileInfoStore.On("GetWithOptions", 0, catchupBatchSize, mock.MatchedBy(func(opt *model.GetFileInfosOptions) bool {
return !opt.OnlyEmptyContent
})).Return(nil, nil).Once()
worker := MakeWorker(jobServer, &trackingApp{}, mockStore)
job := &model.Job{Id: model.NewId(), Data: map[string]string{}}
expectWorkerJobCompletion(mockStore, job)
worker.DoJob(job)
})
}
func TestSchedulerScheduleJob(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
jobServer, mockStore := makeTestJobServer(t)
app := &trackingApp{}
jobServer.RegisterJobType(model.JobTypeExtractContent, MakeWorker(jobServer, app, mockStore), nil)
savedJob := &model.Job{
Id: model.NewId(),
Type: model.JobTypeExtractContent,
Data: map[string]string{catchupJobDataKey: "true"},
}
mockStore.JobStore.On("Save", mock.MatchedBy(func(job *model.Job) bool {
return job.Type == model.JobTypeExtractContent && job.Data[catchupJobDataKey] == "true"
})).Return(savedJob, nil)
scheduler := MakeScheduler(jobServer)
job, appErr := scheduler.ScheduleJob(request.EmptyContext(logger), &model.Config{}, false, nil)
require.Nil(t, appErr)
require.NotNil(t, job)
require.Equal(t, "true", job.Data[catchupJobDataKey])
}