diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go index 25eff56..80ad497 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/controller/plan/converter.go @@ -40,10 +40,10 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { return response } -// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型 -func PlanFromCreateRequest(req *CreatePlanRequest) *models.Plan { +// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 +func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { if req == nil { - return nil + return nil, nil } plan := &models.Plan{ @@ -60,7 +60,7 @@ func PlanFromCreateRequest(req *CreatePlanRequest) *models.Plan { for i, childPlanID := range req.SubPlanIDs { plan.SubPlans[i] = models.SubPlan{ ChildPlanID: childPlanID, - ExecutionOrder: i + 1, // 默认执行顺序 + ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 } } } @@ -69,19 +69,27 @@ func PlanFromCreateRequest(req *CreatePlanRequest) *models.Plan { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { + // 使用来自请求的ExecutionOrder plan.Tasks[i] = TaskFromRequest(&taskReq) - // 设置执行顺序 - plan.Tasks[i].ExecutionOrder = i + 1 } } - return plan + // 1. 首先,执行重复性验证 + if err := plan.ValidateExecutionOrder(); err != nil { + // 如果检测到重复,立即返回错误 + return nil, err + } + + // 2. 然后,调用方法来修复顺序断层 + plan.ReorderSteps() + + return plan, nil } -// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型 -func PlanFromUpdateRequest(req *UpdatePlanRequest) *models.Plan { +// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证 +func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { if req == nil { - return nil + return nil, nil } plan := &models.Plan{ @@ -98,7 +106,7 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) *models.Plan { for i, childPlanID := range req.SubPlanIDs { plan.SubPlans[i] = models.SubPlan{ ChildPlanID: childPlanID, - ExecutionOrder: i + 1, // 默认执行顺序 + ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 } } } @@ -107,13 +115,21 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) *models.Plan { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { + // 使用来自请求的ExecutionOrder plan.Tasks[i] = TaskFromRequest(&taskReq) - // 设置执行顺序 - plan.Tasks[i].ExecutionOrder = i + 1 } } - return plan + // 1. 首先,执行重复性验证 + if err := plan.ValidateExecutionOrder(); err != nil { + // 如果检测到重复,立即返回错误 + return nil, err + } + + // 2. 然后,调用方法来修复顺序断层 + plan.ReorderSteps() + + return plan, nil } // SubPlanToResponse 将SubPlan模型转换为SubPlanResponse diff --git a/internal/app/controller/plan/converter_test.go b/internal/app/controller/plan/converter_test.go new file mode 100644 index 0000000..2626fe8 --- /dev/null +++ b/internal/app/controller/plan/converter_test.go @@ -0,0 +1,459 @@ +package plan_test + +import ( + "testing" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/stretchr/testify/assert" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +func TestPlanToResponse(t *testing.T) { + t.Run("nil plan", func(t *testing.T) { + response := plan.PlanToResponse(nil) + assert.Nil(t, response) + }) + + t.Run("basic plan without associations", func(t *testing.T) { + planModel := &models.Plan{ + Model: gorm.Model{ID: 1}, + Name: "Test Plan", + Description: "A test plan", + ExecutionType: models.PlanExecutionTypeAutomatic, + CronExpression: "0 0 * * *", + ContentType: models.PlanContentTypeTasks, + } + + response := plan.PlanToResponse(planModel) + assert.NotNil(t, response) + assert.Equal(t, uint(1), response.ID) + assert.Equal(t, "Test Plan", response.Name) + assert.Equal(t, "A test plan", response.Description) + assert.Equal(t, models.PlanExecutionTypeAutomatic, response.ExecutionType) + assert.Equal(t, "0 0 * * *", response.CronExpression) + assert.Equal(t, models.PlanContentTypeTasks, response.ContentType) + assert.Empty(t, response.SubPlans) + assert.Empty(t, response.Tasks) + }) + + t.Run("plan with sub plans", func(t *testing.T) { + childPlan := &models.Plan{ + Model: gorm.Model{ID: 2}, + Name: "Child Plan", + ContentType: models.PlanContentTypeTasks, + } + + planModel := &models.Plan{ + Model: gorm.Model{ID: 1}, + Name: "Parent Plan", + ContentType: models.PlanContentTypeSubPlans, + SubPlans: []models.SubPlan{ + { + Model: gorm.Model{ID: 10}, + ParentPlanID: 1, + ChildPlanID: 2, + ExecutionOrder: 1, + ChildPlan: childPlan, + }, + }, + } + + response := plan.PlanToResponse(planModel) + assert.NotNil(t, response) + assert.Equal(t, uint(1), response.ID) + assert.Equal(t, "Parent Plan", response.Name) + assert.Equal(t, models.PlanContentTypeSubPlans, response.ContentType) + assert.Len(t, response.SubPlans, 1) + assert.Empty(t, response.Tasks) + + subPlanResp := response.SubPlans[0] + assert.Equal(t, uint(10), subPlanResp.ID) + assert.Equal(t, uint(1), subPlanResp.ParentPlanID) + assert.Equal(t, uint(2), subPlanResp.ChildPlanID) + assert.Equal(t, 1, subPlanResp.ExecutionOrder) + assert.NotNil(t, subPlanResp.ChildPlan) + assert.Equal(t, "Child Plan", subPlanResp.ChildPlan.Name) + }) + + t.Run("plan with tasks", func(t *testing.T) { + params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`)) + + planModel := &models.Plan{ + Model: gorm.Model{ID: 1}, + Name: "Task Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{ + { + Model: gorm.Model{ID: 10}, + PlanID: 1, + Name: "Task 1", + Description: "First task", + ExecutionOrder: 1, + Type: models.TaskTypeWaiting, + Parameters: params, + }, + }, + } + + response := plan.PlanToResponse(planModel) + assert.NotNil(t, response) + assert.Equal(t, uint(1), response.ID) + assert.Equal(t, "Task Plan", response.Name) + assert.Equal(t, models.PlanContentTypeTasks, response.ContentType) + assert.Len(t, response.Tasks, 1) + assert.Empty(t, response.SubPlans) + + taskResp := response.Tasks[0] + assert.Equal(t, uint(10), taskResp.ID) + assert.Equal(t, uint(1), taskResp.PlanID) + assert.Equal(t, "Task 1", taskResp.Name) + assert.Equal(t, "First task", taskResp.Description) + assert.Equal(t, 1, taskResp.ExecutionOrder) + assert.Equal(t, models.TaskTypeWaiting, taskResp.Type) + assert.Equal(t, controller.Properties(params), taskResp.Parameters) + }) +} + +func TestPlanFromCreateRequest(t *testing.T) { + t.Run("nil request", func(t *testing.T) { + planModel, err := plan.PlanFromCreateRequest(nil) + assert.NoError(t, err) + assert.Nil(t, planModel) + }) + + t.Run("basic plan without associations", func(t *testing.T) { + req := &plan.CreatePlanRequest{ + Name: "Test Plan", + Description: "A test plan", + ExecutionType: models.PlanExecutionTypeAutomatic, + CronExpression: "0 0 * * *", + ContentType: models.PlanContentTypeTasks, + } + + planModel, err := plan.PlanFromCreateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Equal(t, "Test Plan", planModel.Name) + assert.Equal(t, "A test plan", planModel.Description) + assert.Equal(t, models.PlanExecutionTypeAutomatic, planModel.ExecutionType) + assert.Equal(t, "0 0 * * *", planModel.CronExpression) + assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) + assert.Empty(t, planModel.SubPlans) + assert.Empty(t, planModel.Tasks) + }) + + t.Run("plan with sub plan IDs", func(t *testing.T) { + req := &plan.CreatePlanRequest{ + Name: "Parent Plan", + ContentType: models.PlanContentTypeSubPlans, + SubPlanIDs: []uint{2, 3}, + } + + planModel, err := plan.PlanFromCreateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Equal(t, "Parent Plan", planModel.Name) + assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType) + assert.Len(t, planModel.SubPlans, 2) + assert.Empty(t, planModel.Tasks) + + assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID) + assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder) + assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID) + assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder) + }) + + t.Run("plan with tasks", func(t *testing.T) { + params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) + + req := &plan.CreatePlanRequest{ + Name: "Task Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + { + Name: "Task 1", + Description: "First task", + ExecutionOrder: 1, + Type: models.TaskTypeWaiting, + Parameters: params, + }, + }, + } + + planModel, err := plan.PlanFromCreateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Equal(t, "Task Plan", planModel.Name) + assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) + assert.Len(t, planModel.Tasks, 1) + assert.Empty(t, planModel.SubPlans) + + task := planModel.Tasks[0] + assert.Equal(t, "Task 1", task.Name) + assert.Equal(t, "First task", task.Description) + assert.Equal(t, 1, task.ExecutionOrder) + assert.Equal(t, models.TaskTypeWaiting, task.Type) + assert.Equal(t, datatypes.JSON(params), task.Parameters) + }) + + t.Run("plan with tasks with gapped execution order", func(t *testing.T) { + req := &plan.CreatePlanRequest{ + Name: "Task Plan with Gaps", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + {Name: "Task 3", ExecutionOrder: 5}, + {Name: "Task 1", ExecutionOrder: 2}, + }, + } + + planModel, err := plan.PlanFromCreateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Len(t, planModel.Tasks, 2) + + // After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered. + assert.Equal(t, "Task 1", planModel.Tasks[0].Name) + assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder) + assert.Equal(t, "Task 3", planModel.Tasks[1].Name) + assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder) + }) + + t.Run("plan with duplicate task execution order", func(t *testing.T) { + req := &plan.CreatePlanRequest{ + Name: "Invalid Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + {Name: "Task 1", ExecutionOrder: 1}, + {Name: "Task 2", ExecutionOrder: 1}, // Duplicate order + }, + } + + planModel, err := plan.PlanFromCreateRequest(req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "任务执行顺序重复") + assert.Nil(t, planModel) + }) +} + +func TestPlanFromUpdateRequest(t *testing.T) { + t.Run("nil request", func(t *testing.T) { + planModel, err := plan.PlanFromUpdateRequest(nil) + assert.NoError(t, err) + assert.Nil(t, planModel) + }) + + t.Run("basic plan without associations", func(t *testing.T) { + req := &plan.UpdatePlanRequest{ + Name: "Updated Plan", + Description: "An updated plan", + ExecutionType: models.PlanExecutionTypeManual, + CronExpression: "0 30 * * *", + ContentType: models.PlanContentTypeTasks, + } + + planModel, err := plan.PlanFromUpdateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Equal(t, "Updated Plan", planModel.Name) + assert.Equal(t, "An updated plan", planModel.Description) + assert.Equal(t, models.PlanExecutionTypeManual, planModel.ExecutionType) + assert.Equal(t, "0 30 * * *", planModel.CronExpression) + assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) + assert.Empty(t, planModel.SubPlans) + assert.Empty(t, planModel.Tasks) + }) + + t.Run("plan with sub plan IDs", func(t *testing.T) { + req := &plan.UpdatePlanRequest{ + Name: "Updated Parent Plan", + ContentType: models.PlanContentTypeSubPlans, + SubPlanIDs: []uint{2, 3}, + } + + planModel, err := plan.PlanFromUpdateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + + assert.Equal(t, "Updated Parent Plan", planModel.Name) + assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType) + assert.Len(t, planModel.SubPlans, 2) + assert.Empty(t, planModel.Tasks) + + assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID) + assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder) + assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID) + assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder) + }) + + t.Run("plan with tasks", func(t *testing.T) { + params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) + + req := &plan.UpdatePlanRequest{ + Name: "Updated Task Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + { + Name: "Task 1", + Description: "First task", + ExecutionOrder: 1, + Type: models.TaskTypeWaiting, + Parameters: params, + }, + }, + } + + planModel, err := plan.PlanFromUpdateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Equal(t, "Updated Task Plan", planModel.Name) + assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) + assert.Len(t, planModel.Tasks, 1) + assert.Empty(t, planModel.SubPlans) + + task := planModel.Tasks[0] + assert.Equal(t, "Task 1", task.Name) + assert.Equal(t, 1, task.ExecutionOrder) + assert.Equal(t, datatypes.JSON(params), task.Parameters) + }) + + t.Run("plan with duplicate task execution order", func(t *testing.T) { + req := &plan.UpdatePlanRequest{ + Name: "Invalid Updated Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + {Name: "Task 1", ExecutionOrder: 1}, + {Name: "Task 2", ExecutionOrder: 1}, // Duplicate order + }, + } + + planModel, err := plan.PlanFromUpdateRequest(req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "任务执行顺序重复") + assert.Nil(t, planModel) + }) + + t.Run("plan with tasks with gapped execution order", func(t *testing.T) { + req := &plan.UpdatePlanRequest{ + Name: "Updated Task Plan with Gaps", + ContentType: models.PlanContentTypeTasks, + Tasks: []plan.TaskRequest{ + {Name: "Task 3", ExecutionOrder: 5}, + {Name: "Task 1", ExecutionOrder: 2}, + }, + } + + planModel, err := plan.PlanFromUpdateRequest(req) + assert.NoError(t, err) + assert.NotNil(t, planModel) + assert.Len(t, planModel.Tasks, 2) + + // After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered. + assert.Equal(t, "Task 1", planModel.Tasks[0].Name) + assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder) + assert.Equal(t, "Task 3", planModel.Tasks[1].Name) + assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder) + }) +} + +func TestSubPlanToResponse(t *testing.T) { + t.Run("nil sub plan", func(t *testing.T) { + response := plan.SubPlanToResponse(nil) + assert.Equal(t, plan.SubPlanResponse{}, response) + }) + + t.Run("sub plan without child plan", func(t *testing.T) { + subPlan := &models.SubPlan{ + Model: gorm.Model{ID: 10}, + ParentPlanID: 1, + ChildPlanID: 2, + ExecutionOrder: 1, + } + + response := plan.SubPlanToResponse(subPlan) + assert.Equal(t, uint(10), response.ID) + assert.Equal(t, uint(1), response.ParentPlanID) + assert.Equal(t, uint(2), response.ChildPlanID) + assert.Equal(t, 1, response.ExecutionOrder) + assert.Nil(t, response.ChildPlan) + }) + + t.Run("sub plan with child plan", func(t *testing.T) { + childPlan := &models.Plan{ + Model: gorm.Model{ID: 2}, + Name: "Child Plan", + } + + subPlan := &models.SubPlan{ + Model: gorm.Model{ID: 10}, + ParentPlanID: 1, + ChildPlanID: 2, + ExecutionOrder: 1, + ChildPlan: childPlan, + } + + response := plan.SubPlanToResponse(subPlan) + assert.Equal(t, uint(10), response.ID) + assert.Equal(t, uint(1), response.ParentPlanID) + assert.Equal(t, uint(2), response.ChildPlanID) + assert.Equal(t, 1, response.ExecutionOrder) + assert.NotNil(t, response.ChildPlan) + assert.Equal(t, "Child Plan", response.ChildPlan.Name) + }) +} + +func TestTaskToResponse(t *testing.T) { + t.Run("nil task", func(t *testing.T) { + response := plan.TaskToResponse(nil) + assert.Equal(t, plan.TaskResponse{}, response) + }) + + t.Run("task with parameters", func(t *testing.T) { + params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`)) + task := &models.Task{ + Model: gorm.Model{ID: 10}, + PlanID: 1, + Name: "Test Task", + Description: "A test task", + ExecutionOrder: 1, + Type: models.TaskTypeWaiting, + Parameters: params, + } + + response := plan.TaskToResponse(task) + assert.Equal(t, uint(10), response.ID) + assert.Equal(t, uint(1), response.PlanID) + assert.Equal(t, "Test Task", response.Name) + assert.Equal(t, "A test task", response.Description) + assert.Equal(t, 1, response.ExecutionOrder) + assert.Equal(t, models.TaskTypeWaiting, response.Type) + assert.Equal(t, controller.Properties(params), response.Parameters) + }) +} + +func TestTaskFromRequest(t *testing.T) { + t.Run("nil request", func(t *testing.T) { + task := plan.TaskFromRequest(nil) + assert.Equal(t, models.Task{}, task) + }) + + t.Run("task with parameters", func(t *testing.T) { + params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) + req := &plan.TaskRequest{ + Name: "Test Task", + Description: "A test task", + ExecutionOrder: 1, + Type: models.TaskTypeWaiting, + Parameters: params, + } + + task := plan.TaskFromRequest(req) + assert.Equal(t, "Test Task", task.Name) + assert.Equal(t, "A test task", task.Description) + assert.Equal(t, 1, task.ExecutionOrder) + assert.Equal(t, models.TaskTypeWaiting, task.Type) + assert.Equal(t, datatypes.JSON(params), task.Parameters) + }) +}