mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
848884d8cd
按项目、来源、运行时与上传记录锁序补偿异常中断遗留的部署包。\n同时隐藏并保护系统内部排程,避免通用任务管理入口修改 scanner。
732 lines
22 KiB
Go
732 lines
22 KiB
Go
// Copyright 2025 linux.do
|
|
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package task
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
|
uploadtask "github.com/Rain-kl/Wavelet/internal/apps/upload/task"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/user"
|
|
"github.com/Rain-kl/Wavelet/internal/bootstrap"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/Rain-kl/Wavelet/internal/task"
|
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/hibiken/asynq"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
|
)
|
|
|
|
const (
|
|
testInternalOnlyTaskType = "test_internal_only_admin"
|
|
testInternalOnlyAsynqTask = "test:internal_only_admin"
|
|
)
|
|
|
|
func registerInternalOnlyTaskMeta() {
|
|
task.RegisterTaskMeta(task.TaskMeta{
|
|
Type: testInternalOnlyTaskType,
|
|
AsynqTask: testInternalOnlyAsynqTask,
|
|
Name: "内部测试任务",
|
|
InternalOnly: true,
|
|
})
|
|
}
|
|
|
|
func setupTaskTestEnvironment(t *testing.T) func() {
|
|
_, mr, cleanup := testhelper.SetupTestEnvironment(t)
|
|
bootstrap.RegisterTasks()
|
|
task.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{
|
|
Addr: mr.Addr(),
|
|
})
|
|
return func() {
|
|
if task.AsynqClient != nil {
|
|
_ = task.AsynqClient.Close()
|
|
task.AsynqClient = nil
|
|
}
|
|
cleanup()
|
|
}
|
|
}
|
|
|
|
func setupTestRouter(authUser *model.User) *gin.Engine {
|
|
r := testhelper.NewTestGinEngine()
|
|
adminGroup := r.Group("/api/v1/admin")
|
|
|
|
// Mock authentication middleware
|
|
adminGroup.Use(func(c *gin.Context) {
|
|
if authUser != nil {
|
|
oauth.SetToContext(c, oauth.UserObjKey, authUser)
|
|
}
|
|
c.Next()
|
|
})
|
|
|
|
adminGroup.GET("/tasks/types", ListTaskTypes)
|
|
adminGroup.POST("/tasks/dispatch", DispatchTask)
|
|
adminGroup.GET("/tasks/executions", ListTaskExecutions)
|
|
adminGroup.GET("/tasks/executions/:id", GetTaskExecution)
|
|
adminGroup.POST("/tasks/executions/:id/retry", RetryTask)
|
|
adminGroup.GET("/tasks/schedules", ListSchedules)
|
|
adminGroup.POST("/tasks/schedules", CreateSchedule)
|
|
adminGroup.PUT("/tasks/schedules/:id", UpdateSchedule)
|
|
adminGroup.DELETE("/tasks/schedules/:id", DeleteSchedule)
|
|
return r
|
|
}
|
|
|
|
func TestListTaskTypes(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
registerInternalOnlyTaskMeta()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/types", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200 OK, got %d", w.Code)
|
|
}
|
|
|
|
var resp response.Any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var taskMetas []task.TaskMeta
|
|
_ = json.Unmarshal(dataBytes, &taskMetas)
|
|
|
|
if len(taskMetas) == 0 {
|
|
t.Error("expected at least one dispatchable task type")
|
|
}
|
|
|
|
foundCleanup := false
|
|
foundWarmImageCache := false
|
|
for _, m := range taskMetas {
|
|
if m.Type == testInternalOnlyTaskType {
|
|
t.Errorf("internal-only task type %s must not be listed", testInternalOnlyTaskType)
|
|
}
|
|
if m.Type == uploadtask.TaskTypeSystemCleanup {
|
|
foundCleanup = true
|
|
}
|
|
if m.Type == uploadtask.TaskTypeWarmImageCache {
|
|
foundWarmImageCache = true
|
|
}
|
|
}
|
|
if !foundCleanup {
|
|
t.Errorf("expected task type %s to be listed", uploadtask.TaskTypeSystemCleanup)
|
|
}
|
|
if !foundWarmImageCache {
|
|
t.Errorf("expected task type %s to be listed", uploadtask.TaskTypeWarmImageCache)
|
|
}
|
|
}
|
|
|
|
func TestInternalOnlyTaskAdminBoundaries(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
registerInternalOnlyTaskMeta()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
ctx := context.Background()
|
|
|
|
t.Run("list hides internal-only schedule", func(t *testing.T) {
|
|
internalSchedule := &model.Schedule{
|
|
Name: "隐藏的系统内部排程",
|
|
TaskType: testInternalOnlyTaskType,
|
|
Cron: "*/5 * * * *",
|
|
Payload: "{}",
|
|
IsActive: true,
|
|
}
|
|
publicSchedule := &model.Schedule{
|
|
Name: "可见的公开排程",
|
|
TaskType: uploadtask.TaskTypeSystemCleanup,
|
|
Cron: "0 * * * *",
|
|
Payload: "{}",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, model.CreateSchedule(ctx, internalSchedule))
|
|
require.NoError(t, model.CreateSchedule(ctx, publicSchedule))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/tasks/schedules", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp response.Any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
data, err := json.Marshal(resp.Data)
|
|
require.NoError(t, err)
|
|
var schedules []model.Schedule
|
|
require.NoError(t, json.Unmarshal(data, &schedules))
|
|
assert.NotContains(t, scheduleIDs(schedules), internalSchedule.ID)
|
|
assert.Contains(t, scheduleIDs(schedules), publicSchedule.ID)
|
|
})
|
|
|
|
t.Run("dispatch rejects internal-only task", func(t *testing.T) {
|
|
body, err := json.Marshal(DispatchTaskRequest{TaskType: testInternalOnlyTaskType})
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/tasks/dispatch", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp response.Any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, InvalidTaskType, resp.ErrorMsg)
|
|
})
|
|
|
|
t.Run("create schedule rejects internal-only task", func(t *testing.T) {
|
|
isActive := true
|
|
body, err := json.Marshal(CreateScheduleRequest{
|
|
Name: "内部任务排程",
|
|
TaskType: testInternalOnlyTaskType,
|
|
Cron: "0 * * * *",
|
|
IsActive: &isActive,
|
|
})
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/tasks/schedules", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp response.Any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, InvalidTaskType, resp.ErrorMsg)
|
|
})
|
|
|
|
t.Run("update cannot convert existing internal schedule to public task", func(t *testing.T) {
|
|
schedule := &model.Schedule{
|
|
Name: "系统内部排程",
|
|
TaskType: testInternalOnlyTaskType,
|
|
Cron: "0 * * * *",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, model.CreateSchedule(ctx, schedule))
|
|
isActive := false
|
|
body, err := json.Marshal(UpdateScheduleRequest{
|
|
Name: "尝试修改内部排程",
|
|
TaskType: uploadtask.TaskTypeSystemCleanup,
|
|
Cron: "5 * * * *",
|
|
IsActive: &isActive,
|
|
})
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(
|
|
http.MethodPut,
|
|
fmt.Sprintf("/api/v1/admin/tasks/schedules/%d", schedule.ID),
|
|
bytes.NewReader(body),
|
|
)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
unchanged, err := model.GetScheduleByID(ctx, schedule.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "系统内部排程", unchanged.Name)
|
|
assert.Equal(t, testInternalOnlyTaskType, unchanged.TaskType)
|
|
assert.True(t, unchanged.IsActive)
|
|
})
|
|
|
|
t.Run("update public schedule rejects internal-only target task", func(t *testing.T) {
|
|
schedule := &model.Schedule{
|
|
Name: "公开排程",
|
|
TaskType: uploadtask.TaskTypeSystemCleanup,
|
|
Cron: "0 * * * *",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, model.CreateSchedule(ctx, schedule))
|
|
isActive := true
|
|
body, err := json.Marshal(UpdateScheduleRequest{
|
|
Name: "尝试切入内部任务",
|
|
TaskType: testInternalOnlyTaskType,
|
|
Cron: "10 * * * *",
|
|
IsActive: &isActive,
|
|
})
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(
|
|
http.MethodPut,
|
|
fmt.Sprintf("/api/v1/admin/tasks/schedules/%d", schedule.ID),
|
|
bytes.NewReader(body),
|
|
)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
unchanged, err := model.GetScheduleByID(ctx, schedule.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "公开排程", unchanged.Name)
|
|
assert.Equal(t, uploadtask.TaskTypeSystemCleanup, unchanged.TaskType)
|
|
})
|
|
|
|
t.Run("delete rejects internal-only schedule", func(t *testing.T) {
|
|
schedule := &model.Schedule{
|
|
Name: "不可删除的系统内部排程",
|
|
TaskType: testInternalOnlyTaskType,
|
|
Cron: "*/5 * * * *",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, model.CreateSchedule(ctx, schedule))
|
|
req := httptest.NewRequest(
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v1/admin/tasks/schedules/%d", schedule.ID),
|
|
nil,
|
|
)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp response.Any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, InvalidTaskType, resp.ErrorMsg)
|
|
preserved, err := model.GetScheduleByID(ctx, schedule.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testInternalOnlyTaskType, preserved.TaskType)
|
|
})
|
|
|
|
t.Run("delete missing schedule returns not found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/tasks/schedules/999999", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
var resp response.Any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(t, ScheduleNotFound, resp.ErrorMsg)
|
|
})
|
|
|
|
t.Run("delete public schedule remains allowed", func(t *testing.T) {
|
|
schedule := &model.Schedule{
|
|
Name: "可删除的公开排程",
|
|
TaskType: uploadtask.TaskTypeSystemCleanup,
|
|
Cron: "0 * * * *",
|
|
IsActive: false,
|
|
}
|
|
require.NoError(t, model.CreateSchedule(ctx, schedule))
|
|
req := httptest.NewRequest(
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v1/admin/tasks/schedules/%d", schedule.ID),
|
|
nil,
|
|
)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
_, err := model.GetScheduleByID(ctx, schedule.ID)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func scheduleIDs(schedules []model.Schedule) []uint64 {
|
|
ids := make([]uint64, 0, len(schedules))
|
|
for _, schedule := range schedules {
|
|
ids = append(ids, schedule.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func TestDispatchTask(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
|
|
t.Run("dispatch valid task successfully", func(t *testing.T) {
|
|
payload := DispatchTaskRequest{
|
|
TaskType: uploadtask.TaskTypeSystemCleanup,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String())
|
|
|
|
var resp response.Any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Empty(t, resp.ErrorMsg)
|
|
assert.NotNil(t, resp.Data)
|
|
|
|
// 返回的 data 应该是 taskID
|
|
taskID, ok := resp.Data.(string)
|
|
assert.True(t, ok)
|
|
assert.NotEmpty(t, taskID)
|
|
})
|
|
|
|
t.Run("dispatch send_email task successfully with valid payload", func(t *testing.T) {
|
|
payload := DispatchTaskRequest{
|
|
TaskType: user.TaskTypeSendEmail,
|
|
Payload: `{"to":"receiver@example.com","subject":"Test Subject","body":"Test Body"}`,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String())
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Empty(t, resp.ErrorMsg)
|
|
assert.NotNil(t, resp.Data)
|
|
})
|
|
|
|
t.Run("dispatch send_email task failure with invalid payload json", func(t *testing.T) {
|
|
payload := DispatchTaskRequest{
|
|
TaskType: user.TaskTypeSendEmail,
|
|
Payload: `{"to":`,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp.ErrorMsg, "无效的 JSON 格式")
|
|
})
|
|
|
|
t.Run("dispatch send_email task failure with missing fields", func(t *testing.T) {
|
|
payload := DispatchTaskRequest{
|
|
TaskType: user.TaskTypeSendEmail,
|
|
Payload: `{"to":"","subject":"Test","body":"Test"}`,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp.ErrorMsg, "不能为空")
|
|
})
|
|
|
|
t.Run("dispatch invalid task type failure", func(t *testing.T) {
|
|
payload := DispatchTaskRequest{
|
|
TaskType: "invalid_task_type",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Equal(t, InvalidTaskType, resp.ErrorMsg)
|
|
})
|
|
|
|
t.Run("dispatch with empty body failure", func(t *testing.T) {
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer([]byte("{}")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestListTaskExecutions(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
ctx := context.Background()
|
|
|
|
// 准备测试数据
|
|
now := time.Now()
|
|
records := []*model.TaskExecution{
|
|
{TaskID: "exec_001", TaskType: "system:cleanup", TaskName: "系统垃圾清理", Status: model.TaskExecutionStatusSucceeded, TriggeredBy: "manual", Duration: 1500, Result: "清理完成", StartedAt: &now, FinishedAt: &now},
|
|
{TaskID: "exec_002", TaskType: "system:cleanup", TaskName: "系统垃圾清理", Status: model.TaskExecutionStatusFailed, TriggeredBy: "system", ErrorMessage: "连接超时", StartedAt: &now, FinishedAt: &now},
|
|
{TaskID: "exec_003", TaskType: "system:cleanup", TaskName: "系统垃圾清理", Status: model.TaskExecutionStatusPending, TriggeredBy: "manual", Retryable: true, MaxRetry: 3},
|
|
}
|
|
for _, r := range records {
|
|
err := model.CreateTaskExecution(ctx, r)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("list all executions", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var data map[string]interface{}
|
|
json.Unmarshal(dataBytes, &data)
|
|
|
|
assert.Equal(t, float64(3), data["total"])
|
|
})
|
|
|
|
t.Run("filter by status", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions?status=failed", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var data map[string]interface{}
|
|
json.Unmarshal(dataBytes, &data)
|
|
|
|
assert.Equal(t, float64(1), data["total"])
|
|
})
|
|
|
|
t.Run("filter by task_type (asynq task name)", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions?task_type=system:cleanup", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var data map[string]interface{}
|
|
json.Unmarshal(dataBytes, &data)
|
|
|
|
assert.Equal(t, float64(3), data["total"])
|
|
})
|
|
|
|
t.Run("filter by task_type (management task type)", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions?task_type=system_cleanup", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var data map[string]interface{}
|
|
json.Unmarshal(dataBytes, &data)
|
|
|
|
assert.Equal(t, float64(3), data["total"])
|
|
})
|
|
|
|
t.Run("pagination", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions?page=1&page_size=2", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var data map[string]interface{}
|
|
json.Unmarshal(dataBytes, &data)
|
|
|
|
assert.Equal(t, float64(3), data["total"])
|
|
})
|
|
}
|
|
|
|
func TestGetTaskExecution(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
ctx := context.Background()
|
|
|
|
// 创建测试记录
|
|
execution := &model.TaskExecution{
|
|
TaskID: "detail_001",
|
|
TaskType: "system:cleanup",
|
|
TaskName: "系统垃圾清理",
|
|
Status: model.TaskExecutionStatusSucceeded,
|
|
Log: "[10:00:01] 开始扫描\n[10:00:02] 找到 50 个文件\n[10:00:03] 清理完成",
|
|
Result: "共清理 50 个文件",
|
|
Duration: 2000,
|
|
Retryable: true,
|
|
MaxRetry: 3,
|
|
TriggeredBy: "manual",
|
|
}
|
|
err := model.CreateTaskExecution(ctx, execution)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("get existing execution", func(t *testing.T) {
|
|
url := fmt.Sprintf("/api/v1/admin/tasks/executions/%d", execution.ID)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var detail model.TaskExecution
|
|
json.Unmarshal(dataBytes, &detail)
|
|
|
|
assert.Equal(t, "detail_001", detail.TaskID)
|
|
assert.Equal(t, model.TaskExecutionStatusSucceeded, detail.Status)
|
|
assert.Contains(t, detail.Log, "开始扫描")
|
|
assert.Contains(t, detail.Log, "清理完成")
|
|
assert.Equal(t, int64(2000), detail.Duration)
|
|
})
|
|
|
|
t.Run("get non-existent execution", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions/99999999", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
})
|
|
|
|
t.Run("invalid ID format", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/executions/invalid", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestRetryTask(t *testing.T) {
|
|
cleanup := setupTaskTestEnvironment(t)
|
|
defer cleanup()
|
|
|
|
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
|
router := setupTestRouter(adminUser)
|
|
ctx := context.Background()
|
|
|
|
t.Run("retry failed task successfully", func(t *testing.T) {
|
|
now := time.Now()
|
|
execution := &model.TaskExecution{
|
|
TaskID: "retry_api_001",
|
|
TaskType: "system:cleanup",
|
|
TaskName: "系统垃圾清理",
|
|
Status: model.TaskExecutionStatusFailed,
|
|
ErrorMessage: "S3 连接超时",
|
|
Retryable: true,
|
|
MaxRetry: 3,
|
|
RetryCount: 0,
|
|
TriggeredBy: "manual",
|
|
StartedAt: &now,
|
|
FinishedAt: &now,
|
|
}
|
|
err := model.CreateTaskExecution(ctx, execution)
|
|
require.NoError(t, err)
|
|
|
|
url := fmt.Sprintf("/api/v1/admin/tasks/executions/%d/retry", execution.ID)
|
|
req, _ := http.NewRequest("POST", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp response.Any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Empty(t, resp.ErrorMsg)
|
|
assert.NotNil(t, resp.Data)
|
|
|
|
// 验证新记录
|
|
newTaskID, ok := resp.Data.(string)
|
|
assert.True(t, ok)
|
|
assert.NotEmpty(t, newTaskID)
|
|
|
|
newExecution, err := model.GetTaskExecutionByTaskID(ctx, newTaskID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, newExecution.RetryCount)
|
|
assert.Equal(t, "retry", newExecution.TriggeredBy)
|
|
})
|
|
|
|
t.Run("retry succeeded task fails", func(t *testing.T) {
|
|
execution := &model.TaskExecution{
|
|
TaskID: "retry_succeeded_001",
|
|
TaskType: "system:cleanup",
|
|
TaskName: "系统垃圾清理",
|
|
Status: model.TaskExecutionStatusSucceeded,
|
|
Retryable: true,
|
|
MaxRetry: 3,
|
|
TriggeredBy: "manual",
|
|
}
|
|
err := model.CreateTaskExecution(ctx, execution)
|
|
require.NoError(t, err)
|
|
|
|
url := fmt.Sprintf("/api/v1/admin/tasks/executions/%d/retry", execution.ID)
|
|
req, _ := http.NewRequest("POST", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("retry non-retryable task fails", func(t *testing.T) {
|
|
execution := &model.TaskExecution{
|
|
TaskID: "retry_not_allowed_001",
|
|
TaskType: "system:cleanup",
|
|
TaskName: "系统垃圾清理",
|
|
Status: model.TaskExecutionStatusFailed,
|
|
Retryable: false,
|
|
TriggeredBy: "manual",
|
|
}
|
|
err := model.CreateTaskExecution(ctx, execution)
|
|
require.NoError(t, err)
|
|
|
|
url := fmt.Sprintf("/api/v1/admin/tasks/executions/%d/retry", execution.ID)
|
|
req, _ := http.NewRequest("POST", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("retry non-existent task", func(t *testing.T) {
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/executions/99999999/retry", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
})
|
|
|
|
t.Run("retry with invalid ID", func(t *testing.T) {
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/executions/invalid/retry", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|