snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
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 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
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 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
83
server/channels/jobs/scheduled_recap/scheduler.go
Normal file
83
server/channels/jobs/scheduled_recap/scheduler.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package scheduled_recap
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
// SchedulerPollingInterval defines how often the scheduler polls for due scheduled recaps.
|
||||
const SchedulerPollingInterval = 1 * time.Minute
|
||||
|
||||
// Scheduler polls for due scheduled recaps and creates jobs for them.
|
||||
type Scheduler struct {
|
||||
*jobs.PeriodicScheduler
|
||||
store store.Store
|
||||
jobServer *jobs.JobServer
|
||||
}
|
||||
|
||||
// MakeScheduler creates a new scheduler for scheduled recaps.
|
||||
func MakeScheduler(jobServer *jobs.JobServer, storeInstance store.Store) *Scheduler {
|
||||
isEnabled := func(cfg *model.Config) bool {
|
||||
return cfg.AIRecapsEnabled()
|
||||
}
|
||||
return &Scheduler{
|
||||
PeriodicScheduler: jobs.NewPeriodicScheduler(
|
||||
jobServer,
|
||||
model.JobTypeScheduledRecap,
|
||||
SchedulerPollingInterval,
|
||||
isEnabled,
|
||||
),
|
||||
store: storeInstance,
|
||||
jobServer: jobServer,
|
||||
}
|
||||
}
|
||||
|
||||
// NextScheduleTime overrides to use tight polling interval.
|
||||
func (s *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastJob *model.Job) *time.Time {
|
||||
next := now.Add(SchedulerPollingInterval)
|
||||
return &next
|
||||
}
|
||||
|
||||
// ScheduleJob polls for due scheduled recaps and creates jobs for each.
|
||||
func (s *Scheduler) ScheduleJob(rctx request.CTX, cfg *model.Config, pendingJobs bool, lastJob *model.Job) (*model.Job, *model.AppError) {
|
||||
now := model.GetMillis()
|
||||
dueRecaps, err := s.store.ScheduledRecap().GetDueBefore(now, 100)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to get due scheduled recaps", mlog.Err(err))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, sr := range dueRecaps {
|
||||
// The worker re-fetches the full row by ID, so the job only needs the ID.
|
||||
jobData := model.StringMap{
|
||||
"scheduled_recap_id": sr.Id,
|
||||
}
|
||||
|
||||
job, jobErr := s.jobServer.CreateJobOnceByTypeAndData(
|
||||
rctx,
|
||||
model.JobTypeScheduledRecap,
|
||||
jobData,
|
||||
map[string]string{"scheduled_recap_id": sr.Id},
|
||||
)
|
||||
if jobErr != nil {
|
||||
mlog.Warn("Scheduled recap job creation failed",
|
||||
mlog.String("scheduled_recap_id", sr.Id),
|
||||
mlog.Err(jobErr))
|
||||
continue
|
||||
}
|
||||
if job == nil {
|
||||
mlog.Debug("Scheduled recap job already queued",
|
||||
mlog.String("scheduled_recap_id", sr.Id))
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
69
server/channels/jobs/scheduled_recap/scheduler_test.go
Normal file
69
server/channels/jobs/scheduled_recap/scheduler_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package scheduled_recap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScheduleJobEnqueuesEachDueRecapAndSkipsDuplicateAtomically(t *testing.T) {
|
||||
cfg := &model.Config{}
|
||||
cfg.SetDefaults()
|
||||
cfg.FeatureFlags.EnableAIRecaps = true
|
||||
|
||||
mockStore := &storetest.Store{}
|
||||
t.Cleanup(func() {
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
jobServer := jobs.NewJobServer(&testutils.StaticConfigService{Cfg: cfg}, mockStore, nil, mlog.CreateConsoleTestLogger(t), nil)
|
||||
jobServer.RegisterJobType(model.JobTypeScheduledRecap, jobs.NewSimpleWorker(
|
||||
model.JobTypeScheduledRecap,
|
||||
jobServer,
|
||||
func(logger mlog.LoggerIFace, job *model.Job) error { return nil },
|
||||
func(cfg *model.Config) bool { return true },
|
||||
), nil)
|
||||
|
||||
dueRecap1 := testScheduledRecap(true)
|
||||
dueRecap2 := testScheduledRecap(true)
|
||||
duplicateRecap := *dueRecap1
|
||||
dueRecaps := []*model.ScheduledRecap{dueRecap1, dueRecap2, &duplicateRecap}
|
||||
|
||||
mockStore.ScheduledRecapStore.
|
||||
On("GetDueBefore", mock.AnythingOfType("int64"), 100).
|
||||
Return(dueRecaps, nil)
|
||||
|
||||
for _, sr := range []*model.ScheduledRecap{dueRecap1, dueRecap2} {
|
||||
mockStore.JobStore.
|
||||
On("SaveOnceByTypeAndData", mock.MatchedBy(func(job *model.Job) bool {
|
||||
return job.Type == model.JobTypeScheduledRecap &&
|
||||
len(job.Data) == 1 &&
|
||||
job.Data["scheduled_recap_id"] == sr.Id
|
||||
}), map[string]string{"scheduled_recap_id": sr.Id}).
|
||||
Return(func(job *model.Job, data map[string]string) *model.Job { return job }, nil).
|
||||
Once()
|
||||
}
|
||||
|
||||
mockStore.JobStore.
|
||||
On("SaveOnceByTypeAndData", mock.MatchedBy(func(job *model.Job) bool {
|
||||
return job.Type == model.JobTypeScheduledRecap &&
|
||||
job.Data["scheduled_recap_id"] == dueRecap1.Id
|
||||
}), map[string]string{"scheduled_recap_id": dueRecap1.Id}).
|
||||
Return(nil, nil).
|
||||
Once()
|
||||
|
||||
scheduler := MakeScheduler(jobServer, mockStore)
|
||||
job, appErr := scheduler.ScheduleJob(request.EmptyContext(mlog.CreateConsoleTestLogger(t)), cfg, false, nil)
|
||||
require.Nil(t, appErr)
|
||||
require.Nil(t, job)
|
||||
}
|
||||
138
server/channels/jobs/scheduled_recap/worker.go
Normal file
138
server/channels/jobs/scheduled_recap/worker.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package scheduled_recap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
)
|
||||
|
||||
// AppIface defines the app methods required by the scheduled recap worker.
|
||||
// This interface will be implemented by the App layer in 03-02.
|
||||
type AppIface interface {
|
||||
CreateRecapFromSchedule(rctx request.CTX, scheduledRecap *model.ScheduledRecap) (*model.Recap, *model.AppError)
|
||||
}
|
||||
|
||||
// MakeWorker creates a new worker for processing scheduled recap jobs.
|
||||
func MakeWorker(jobServer *jobs.JobServer, storeInstance store.Store, app AppIface) *jobs.SimpleWorker {
|
||||
const workerName = "ScheduledRecap"
|
||||
|
||||
isEnabled := func(cfg *model.Config) bool {
|
||||
return cfg.AIRecapsEnabled()
|
||||
}
|
||||
|
||||
execute := func(logger mlog.LoggerIFace, job *model.Job) error {
|
||||
defer jobServer.HandleJobPanic(logger, job)
|
||||
return processScheduledRecapJob(logger, job, storeInstance, app)
|
||||
}
|
||||
|
||||
return jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled)
|
||||
}
|
||||
|
||||
func processScheduledRecapJob(logger mlog.LoggerIFace, job *model.Job, storeInstance store.Store, app AppIface) error {
|
||||
scheduledRecapID := job.Data["scheduled_recap_id"]
|
||||
|
||||
// Get the scheduled recap
|
||||
sr, err := storeInstance.ScheduledRecap().Get(scheduledRecapID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scheduled recap not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify still enabled
|
||||
if !sr.Enabled {
|
||||
logger.Info("Scheduled recap is disabled, skipping",
|
||||
mlog.String("scheduled_recap_id", scheduledRecapID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateRecapFromSchedule performs the atomic daily-limit check; an over-limit
|
||||
// run surfaces as the max_recaps_reached error, which we treat as a skip.
|
||||
rctx := request.EmptyContext(logger)
|
||||
_, appErr := app.CreateRecapFromSchedule(rctx, sr)
|
||||
if appErr != nil {
|
||||
if appErr.Id == "app.recap.max_recaps_reached.app_error" {
|
||||
if saveErr := saveSkippedRecap(storeInstance, sr); saveErr != nil {
|
||||
logger.Error("Failed to save skipped recap", mlog.Err(saveErr))
|
||||
return fmt.Errorf("failed to save skipped recap: %w", saveErr)
|
||||
}
|
||||
logger.Info("Scheduled recap skipped due to daily limit",
|
||||
mlog.String("scheduled_recap_id", scheduledRecapID),
|
||||
mlog.String("user_id", sr.UserId))
|
||||
return finalizeSchedule(logger, storeInstance, sr)
|
||||
}
|
||||
return fmt.Errorf("failed to create recap from schedule: %w", appErr)
|
||||
}
|
||||
|
||||
logger.Info("Scheduled recap executed successfully",
|
||||
mlog.String("scheduled_recap_id", scheduledRecapID))
|
||||
|
||||
return finalizeSchedule(logger, storeInstance, sr)
|
||||
}
|
||||
|
||||
// finalizeSchedule advances the schedule to its next run and, for non-recurring
|
||||
// recaps, disables it last so a one-shot recap is never left enabled.
|
||||
func finalizeSchedule(logger mlog.LoggerIFace, storeInstance store.Store, sr *model.ScheduledRecap) error {
|
||||
if err := advanceSchedule(logger, storeInstance, sr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sr.IsRecurring {
|
||||
logger.Info("Disabling non-recurring scheduled recap",
|
||||
mlog.String("scheduled_recap_id", sr.Id))
|
||||
if setErr := storeInstance.ScheduledRecap().SetEnabled(sr.Id, false); setErr != nil {
|
||||
return fmt.Errorf("failed to disable non-recurring scheduled recap: %w", setErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// advanceSchedule computes the next run time and marks the scheduled recap as executed.
|
||||
// If the next run time can't be computed, the recap is disabled.
|
||||
func advanceSchedule(logger mlog.LoggerIFace, storeInstance store.Store, sr *model.ScheduledRecap) error {
|
||||
nextRunAt, computeErr := sr.ComputeNextRunAt(time.Now())
|
||||
if computeErr != nil {
|
||||
logger.Error("Failed to compute next run time",
|
||||
mlog.String("scheduled_recap_id", sr.Id),
|
||||
mlog.Err(computeErr))
|
||||
if setErr := storeInstance.ScheduledRecap().SetEnabled(sr.Id, false); setErr != nil {
|
||||
return fmt.Errorf("failed to disable scheduled recap after next-run computation failure: %w", setErr)
|
||||
}
|
||||
return fmt.Errorf("failed to compute next run time: %w", computeErr)
|
||||
}
|
||||
|
||||
if markErr := storeInstance.ScheduledRecap().MarkExecuted(sr.Id, model.GetMillis(), nextRunAt); markErr != nil {
|
||||
logger.Error("Failed to mark as executed",
|
||||
mlog.String("scheduled_recap_id", sr.Id),
|
||||
mlog.Err(markErr))
|
||||
return fmt.Errorf("failed to mark scheduled recap as executed: %w", markErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveSkippedRecap(storeInstance store.Store, sr *model.ScheduledRecap) error {
|
||||
skippedRecap := &model.Recap{
|
||||
Id: model.NewId(),
|
||||
UserId: sr.UserId,
|
||||
Title: sr.Title,
|
||||
Status: model.RecapStatusSkipped,
|
||||
SkipReason: model.SkipReasonDailyLimit,
|
||||
ScheduledRecapId: sr.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
}
|
||||
|
||||
if _, saveErr := storeInstance.Recap().SaveRecap(skippedRecap); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
server/channels/jobs/scheduled_recap/worker_test.go
Normal file
148
server/channels/jobs/scheduled_recap/worker_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package scheduled_recap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"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/store/storetest/mocks"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockScheduledRecapApp struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockScheduledRecapApp) CreateRecapFromSchedule(rctx request.CTX, scheduledRecap *model.ScheduledRecap) (*model.Recap, *model.AppError) {
|
||||
args := m.Called(rctx, scheduledRecap)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Get(1).(*model.AppError)
|
||||
}
|
||||
return args.Get(0).(*model.Recap), nil
|
||||
}
|
||||
|
||||
func TestProcessScheduledRecapJobReturnsPersistenceErrors(t *testing.T) {
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
|
||||
t.Run("mark executed failure", func(t *testing.T) {
|
||||
scheduledRecap := testScheduledRecap(true)
|
||||
job := &model.Job{Data: map[string]string{"scheduled_recap_id": scheduledRecap.Id}}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
mockScheduledStore := &mocks.ScheduledRecapStore{}
|
||||
mockStore.On("ScheduledRecap").Return(mockScheduledStore)
|
||||
mockScheduledStore.On("Get", scheduledRecap.Id).Return(scheduledRecap, nil)
|
||||
mockScheduledStore.On("MarkExecuted", scheduledRecap.Id, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(errors.New("mark failed"))
|
||||
|
||||
mockApp := &mockScheduledRecapApp{}
|
||||
mockApp.On("CreateRecapFromSchedule", mock.Anything, scheduledRecap).Return(&model.Recap{Id: model.NewId()}, nil)
|
||||
|
||||
err := processScheduledRecapJob(logger, job, mockStore, mockApp)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to mark scheduled recap as executed")
|
||||
mockScheduledStore.AssertExpectations(t)
|
||||
mockApp.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("non recurring disable failure", func(t *testing.T) {
|
||||
scheduledRecap := testScheduledRecap(false)
|
||||
job := &model.Job{Data: map[string]string{"scheduled_recap_id": scheduledRecap.Id}}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
mockScheduledStore := &mocks.ScheduledRecapStore{}
|
||||
mockStore.On("ScheduledRecap").Return(mockScheduledStore)
|
||||
mockScheduledStore.On("Get", scheduledRecap.Id).Return(scheduledRecap, nil)
|
||||
mockScheduledStore.On("MarkExecuted", scheduledRecap.Id, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil)
|
||||
mockScheduledStore.On("SetEnabled", scheduledRecap.Id, false).Return(errors.New("disable failed"))
|
||||
|
||||
mockApp := &mockScheduledRecapApp{}
|
||||
mockApp.On("CreateRecapFromSchedule", mock.Anything, scheduledRecap).Return(&model.Recap{Id: model.NewId()}, nil)
|
||||
|
||||
err := processScheduledRecapJob(logger, job, mockStore, mockApp)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to disable non-recurring scheduled recap")
|
||||
mockScheduledStore.AssertExpectations(t)
|
||||
mockApp.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessScheduledRecapJobDailyLimitSkip(t *testing.T) {
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
limitErr := model.NewAppError("CreateRecapFromSchedule", "app.recap.max_recaps_reached.app_error", nil, "", 429)
|
||||
|
||||
t.Run("recurring schedule advances but stays enabled", func(t *testing.T) {
|
||||
scheduledRecap := testScheduledRecap(true)
|
||||
job := &model.Job{Data: map[string]string{"scheduled_recap_id": scheduledRecap.Id}}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
mockScheduledStore := &mocks.ScheduledRecapStore{}
|
||||
mockRecapStore := &mocks.RecapStore{}
|
||||
mockStore.On("ScheduledRecap").Return(mockScheduledStore)
|
||||
mockStore.On("Recap").Return(mockRecapStore)
|
||||
mockScheduledStore.On("Get", scheduledRecap.Id).Return(scheduledRecap, nil)
|
||||
mockScheduledStore.On("MarkExecuted", scheduledRecap.Id, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil)
|
||||
mockRecapStore.On("SaveRecap", mock.MatchedBy(func(r *model.Recap) bool {
|
||||
return r.Status == model.RecapStatusSkipped && r.ScheduledRecapId == scheduledRecap.Id
|
||||
})).Return(&model.Recap{}, nil)
|
||||
|
||||
mockApp := &mockScheduledRecapApp{}
|
||||
mockApp.On("CreateRecapFromSchedule", mock.Anything, scheduledRecap).Return(nil, limitErr)
|
||||
|
||||
err := processScheduledRecapJob(logger, job, mockStore, mockApp)
|
||||
require.NoError(t, err)
|
||||
mockScheduledStore.AssertExpectations(t)
|
||||
mockRecapStore.AssertExpectations(t)
|
||||
mockApp.AssertExpectations(t)
|
||||
// A recurring schedule must not be disabled on the skip path.
|
||||
mockScheduledStore.AssertNotCalled(t, "SetEnabled", mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("non recurring schedule is disabled on skip", func(t *testing.T) {
|
||||
scheduledRecap := testScheduledRecap(false)
|
||||
job := &model.Job{Data: map[string]string{"scheduled_recap_id": scheduledRecap.Id}}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
mockScheduledStore := &mocks.ScheduledRecapStore{}
|
||||
mockRecapStore := &mocks.RecapStore{}
|
||||
mockStore.On("ScheduledRecap").Return(mockScheduledStore)
|
||||
mockStore.On("Recap").Return(mockRecapStore)
|
||||
mockScheduledStore.On("Get", scheduledRecap.Id).Return(scheduledRecap, nil)
|
||||
mockScheduledStore.On("MarkExecuted", scheduledRecap.Id, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil)
|
||||
mockScheduledStore.On("SetEnabled", scheduledRecap.Id, false).Return(nil)
|
||||
mockRecapStore.On("SaveRecap", mock.MatchedBy(func(r *model.Recap) bool {
|
||||
return r.Status == model.RecapStatusSkipped && r.ScheduledRecapId == scheduledRecap.Id
|
||||
})).Return(&model.Recap{}, nil)
|
||||
|
||||
mockApp := &mockScheduledRecapApp{}
|
||||
mockApp.On("CreateRecapFromSchedule", mock.Anything, scheduledRecap).Return(nil, limitErr)
|
||||
|
||||
err := processScheduledRecapJob(logger, job, mockStore, mockApp)
|
||||
require.NoError(t, err)
|
||||
mockScheduledStore.AssertExpectations(t)
|
||||
mockRecapStore.AssertExpectations(t)
|
||||
mockApp.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduledRecap(isRecurring bool) *model.ScheduledRecap {
|
||||
return &model.ScheduledRecap{
|
||||
Id: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
Title: "Scheduled Recap",
|
||||
DaysOfWeek: model.EveryDay,
|
||||
TimeOfDay: "09:00",
|
||||
Timezone: "America/New_York",
|
||||
TimePeriod: model.TimePeriodLast24h,
|
||||
ChannelMode: model.ChannelModeSpecific,
|
||||
ChannelIds: []string{model.NewId()},
|
||||
AgentId: "test-agent",
|
||||
IsRecurring: isRecurring,
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user