package main import ( "encoding/json" "testing" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/mattermost/mattermost/server/public/pluginapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func newTestTaskStore(t *testing.T) (*taskStore, *plugintest.API) { t.Helper() api := &plugintest.API{} client := pluginapi.NewClient(api, nil) store := newTaskStore(client, nil) return store, api } func TestSaveTask_New(t *testing.T) { store, api := newTestTaskStore(t) task := &DownloadTask{ ID: "task1", BookTitle: "Test Book", Status: TaskStatusPending, CreatedAt: time.Now(), } // pluginapi.KV.Set -> KVSetWithOptions api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8"), mock.Anything).Return(true, nil) // pluginapi.KV.Get -> KVGet (returns []byte, *AppError) api.On("KVGet", "dl_task_index").Return([]byte(nil), (*model.AppError)(nil)) err := store.SaveTask(task) require.NoError(t, err) assert.False(t, task.UpdatedAt.IsZero()) } func TestSaveTask_Existing(t *testing.T) { store, api := newTestTaskStore(t) task := &DownloadTask{ ID: "task1", BookTitle: "Test Book", Status: TaskStatusQueued, } api.On("KVSetWithOptions", "dl_task_task1", mock.AnythingOfType("[]uint8"), mock.Anything).Return(true, nil) indexData, _ := json.Marshal([]string{"task1"}) api.On("KVGet", "dl_task_index").Return(indexData, (*model.AppError)(nil)) err := store.SaveTask(task) require.NoError(t, err) // Index should NOT be re-saved since task1 already in index. api.AssertNotCalled(t, "KVSetWithOptions", "dl_task_index", mock.Anything, mock.Anything) } func TestGetTask_Exists(t *testing.T) { store, api := newTestTaskStore(t) task := &DownloadTask{ ID: "task1", BookTitle: "Test Book", Status: TaskStatusPending, } taskData, _ := json.Marshal(task) api.On("KVGet", "dl_task_task1").Return(taskData, (*model.AppError)(nil)) result, err := store.GetTask("task1") require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, "task1", result.ID) assert.Equal(t, "Test Book", result.BookTitle) } func TestGetTask_NotFound(t *testing.T) { store, api := newTestTaskStore(t) api.On("KVGet", "dl_task_nonexistent").Return([]byte(nil), (*model.AppError)(nil)) result, err := store.GetTask("nonexistent") require.NoError(t, err) assert.Nil(t, result) } func TestDeleteTask(t *testing.T) { store, api := newTestTaskStore(t) // pluginapi.KV.Delete calls KV.Set(key, nil) -> KVSetWithOptions api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return(true, nil) indexData, _ := json.Marshal([]string{"task1", "task2"}) api.On("KVGet", "dl_task_index").Return(indexData, (*model.AppError)(nil)) err := store.DeleteTask("task1") require.NoError(t, err) } func TestListActiveTasks_MixedStatuses(t *testing.T) { store, api := newTestTaskStore(t) taskMap := map[string]*DownloadTask{ "pending": {ID: "pending", Status: TaskStatusPending}, "queued": {ID: "queued", Status: TaskStatusQueued}, "downloading": {ID: "downloading", Status: TaskStatusDownloading}, "complete": {ID: "complete", Status: TaskStatusComplete}, "uploaded": {ID: "uploaded", Status: TaskStatusUploaded}, "failed": {ID: "failed", Status: TaskStatusFailed}, } indexData, _ := json.Marshal([]string{"pending", "queued", "downloading", "complete", "uploaded", "failed"}) api.On("KVGet", "dl_task_index").Return(indexData, (*model.AppError)(nil)) for id, task := range taskMap { taskData, _ := json.Marshal(task) api.On("KVGet", "dl_task_"+id).Return(taskData, (*model.AppError)(nil)) } // For terminal tasks cleanup. // pluginapi.KV.Delete calls KV.Set(key,nil) -> KVSetWithOptions api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return(true, nil) result, err := store.ListActiveTasks() require.NoError(t, err) assert.Len(t, result, 4) ids := make([]string, len(result)) for i, task := range result { ids[i] = task.ID } assert.Contains(t, ids, "pending") assert.Contains(t, ids, "queued") assert.Contains(t, ids, "downloading") assert.Contains(t, ids, "complete") } func TestListActiveTasks_OrphanedIndex(t *testing.T) { store, api := newTestTaskStore(t) indexData, _ := json.Marshal([]string{"orphan"}) api.On("KVGet", "dl_task_index").Return(indexData, (*model.AppError)(nil)) api.On("KVGet", "dl_task_orphan").Return([]byte(nil), (*model.AppError)(nil)) // pluginapi.KV.Delete calls KV.Set(key,nil) -> KVSetWithOptions api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.Anything).Return(true, nil) result, err := store.ListActiveTasks() require.NoError(t, err) assert.Empty(t, result) } func TestListActiveTasks_LogsGetTaskError(t *testing.T) { api := &plugintest.API{} client := pluginapi.NewClient(api, nil) var logged bool store := newTaskStore(client, func(msg string, keyvals ...string) { logged = true assert.Equal(t, "Failed to load task from KV store", msg) }) indexData, _ := json.Marshal([]string{"broken"}) api.On("KVGet", "dl_task_index").Return(indexData, (*model.AppError)(nil)) api.On("KVGet", "dl_task_broken").Return([]byte(nil), model.NewAppError("", "", nil, "kv error", 500)) result, err := store.ListActiveTasks() require.NoError(t, err) assert.Empty(t, result) assert.True(t, logged, "expected logFunc to be called for GetTask error") } func TestEffectiveTitle(t *testing.T) { t.Run("localized title present", func(t *testing.T) { task := &DownloadTask{BookTitle: "Original", LocalizedTitle: "Localized"} assert.Equal(t, "Localized", task.EffectiveTitle()) }) t.Run("localized title empty", func(t *testing.T) { task := &DownloadTask{BookTitle: "Original", LocalizedTitle: ""} assert.Equal(t, "Original", task.EffectiveTitle()) }) }