Files
pig-farm-controller/internal/infra/repository/plan_repository_test.go
2025-09-13 17:49:43 +08:00

1027 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository_test
import (
"errors"
"fmt"
"testing"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
// createTestPlan 是一个辅助函数,用于创建测试计划。
func createTestPlan(name, description string, execType models.PlanExecutionType, contentType models.PlanContentType) models.Plan {
return models.Plan{
Name: name,
Description: description,
ExecutionType: execType,
ContentType: contentType,
}
}
// TestListBasicPlans 测试 ListBasicPlans 方法,确保它能正确返回所有计划的基本信息。
func TestListBasicPlans(t *testing.T) {
tests := []struct {
name string
setupPlans []models.Plan
expectedCount int
expectedError error
}{
{
name: "数据库中没有计划",
setupPlans: []models.Plan{},
expectedCount: 0,
expectedError: nil,
},
{
name: "数据库中有多个计划",
setupPlans: []models.Plan{
createTestPlan("计划 A", "描述 A", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks),
createTestPlan("计划 B", "描述 B", models.PlanExecutionTypeManual, models.PlanContentTypeSubPlans),
},
expectedCount: 2,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := setupTestDB(t)
repo := repository.NewGormPlanRepository(db)
for i := range tt.setupPlans {
err := db.Create(&tt.setupPlans[i]).Error
assert.NoError(t, err, "插入设置计划失败")
}
plans, err := repo.ListBasicPlans()
if tt.expectedError != nil {
assert.Error(t, err)
assert.True(t, errors.Is(err, tt.expectedError))
} else {
assert.NoError(t, err)
assert.Len(t, plans, tt.expectedCount)
if tt.expectedCount > 0 {
// 验证返回的计划是否包含预期的ID
var actualIDs []uint
for _, p := range plans {
actualIDs = append(actualIDs, p.ID)
assert.Empty(t, p.SubPlans, "ListBasicPlans 不应加载子计划")
assert.Empty(t, p.Tasks, "ListBasicPlans 不应加载任务")
}
for _, setupPlan := range tt.setupPlans {
assert.Contains(t, actualIDs, setupPlan.ID, "返回的计划应包含设置计划的ID")
}
}
}
})
}
}
// TestGetBasicPlanByID 测试 GetBasicPlanByID 方法确保它能根据ID正确返回计划的基本信息。
func TestGetBasicPlanByID(t *testing.T) {
tests := []struct {
name string
setupPlan models.Plan
idToFetch uint
expectFound bool
expectedError error
}{
{
name: "通过ID找到计划",
setupPlan: createTestPlan("计划 C", "描述 C", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks),
idToFetch: 0, // 创建后设置
expectFound: true,
},
{
name: "通过ID未找到计划",
setupPlan: models.Plan{}, // 此情况下无需设置计划
idToFetch: 999,
expectFound: false,
expectedError: gorm.ErrRecordNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := setupTestDB(t)
repo := repository.NewGormPlanRepository(db)
if tt.setupPlan.Name != "" { // 仅在有效设置时创建计划
err := db.Create(&tt.setupPlan).Error
assert.NoError(t, err, "插入设置计划失败")
tt.idToFetch = tt.setupPlan.ID // 使用数据库生成的ID
}
fetchedPlan, err := repo.GetBasicPlanByID(tt.idToFetch)
if tt.expectedError != nil {
assert.Error(t, err)
assert.True(t, errors.Is(err, tt.expectedError), "预期错误类型不匹配")
assert.Nil(t, fetchedPlan)
} else {
assert.NoError(t, err)
assert.NotNil(t, fetchedPlan)
assert.Equal(t, tt.setupPlan.Name, fetchedPlan.Name)
assert.Equal(t, tt.setupPlan.Description, fetchedPlan.Description)
assert.Equal(t, tt.setupPlan.ExecutionType, fetchedPlan.ExecutionType)
assert.Equal(t, tt.setupPlan.ContentType, fetchedPlan.ContentType)
assert.Empty(t, fetchedPlan.SubPlans, "GetBasicPlanByID 不应加载子计划")
assert.Empty(t, fetchedPlan.Tasks, "GetBasicPlanByID 不应加载任务")
}
})
}
}
// TestGetPlanByID 测试 GetPlanByID 方法确保它能根据ID正确返回计划的完整信息包括关联数据。
func TestGetPlanByID(t *testing.T) {
type testCase struct {
name string
setupData func(db *gorm.DB) // 用于在测试前插入数据
planID uint
expectedPlan *models.Plan
expectedError string
}
testCases := []testCase{
{
name: "PlanNotFound",
setupData: func(db *gorm.DB) {
// 不插入任何数据
},
planID: 999,
expectedPlan: nil,
expectedError: "record not found",
},
{
name: "PlanWithTasks",
setupData: func(db *gorm.DB) {
// 使用硬编码的ID创建计划使测试可预测
plan := models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan A",
ContentType: models.PlanContentTypeTasks,
}
db.Create(&plan)
// 创建任务它们的ID将由数据库自动生成
db.Create(&models.Task{PlanID: 1, Name: "Task 2", ExecutionOrder: 1})
db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 2})
},
planID: 1,
expectedPlan: &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan A",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
// 期望按 "order" 字段升序排序
{PlanID: 1, Name: "Task 2", ExecutionOrder: 1},
{PlanID: 1, Name: "Task 1", ExecutionOrder: 2},
},
},
expectedError: "",
},
{
name: "PlanWithMultiLevelSubPlans",
setupData: func(db *gorm.DB) {
// 创建一个三层结构的计划
db.Create(&models.Plan{Model: gorm.Model{ID: 20}, Name: "Grandparent Plan", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 21}, Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 22}, Name: "Child Plan With Tasks", ContentType: models.PlanContentTypeTasks})
db.Create(&models.Task{PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1})
// 创建关联关系
db.Create(&models.SubPlan{ParentPlanID: 20, ChildPlanID: 21, ExecutionOrder: 1})
db.Create(&models.SubPlan{ParentPlanID: 21, ChildPlanID: 22, ExecutionOrder: 1})
},
planID: 20,
expectedPlan: &models.Plan{
Model: gorm.Model{ID: 20},
Name: "Grandparent Plan",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{
ParentPlanID: 20,
ChildPlanID: 21,
ExecutionOrder: 1,
ChildPlan: &models.Plan{
Model: gorm.Model{ID: 21},
Name: "Parent Plan",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{
ParentPlanID: 21,
ChildPlanID: 22,
ExecutionOrder: 1,
ChildPlan: &models.Plan{
Model: gorm.Model{ID: 22},
Name: "Child Plan With Tasks",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1},
},
},
},
},
},
},
},
},
expectedError: "",
},
{
name: "UnknownContentType",
setupData: func(db *gorm.DB) {
db.Create(&models.Plan{
Model: gorm.Model{ID: 30},
Name: "Unknown Type Plan",
ContentType: "INVALID_TYPE",
})
},
planID: 30,
expectedPlan: nil,
expectedError: fmt.Sprintf("未知的计划内容类型: INVALID_TYPE; 计划ID: 30"),
},
// 新增场景:测试空的关联数据
{
name: "PlanWithTasksType_ButNoTasks",
setupData: func(db *gorm.DB) {
db.Create(&models.Plan{
Model: gorm.Model{ID: 50},
Name: "Plan with empty tasks",
ContentType: models.PlanContentTypeTasks,
})
},
planID: 50,
expectedPlan: &models.Plan{
Model: gorm.Model{ID: 50},
Name: "Plan with empty tasks",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{}, // 期望一个空的切片
},
expectedError: "",
},
// 新增场景:测试复杂的同级排序
{
name: "PlanWithSubPlans_ComplexSorting",
setupData: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 60}, Name: "Main Plan For Sorting", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks})
db.Create(&models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks})
db.Create(&models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks})
// 故意打乱顺序插入
db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3})
db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1})
db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2})
},
planID: 60,
expectedPlan: &models.Plan{
Model: gorm.Model{ID: 60},
Name: "Main Plan For Sorting",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}},
{ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}},
{ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3, ChildPlan: &models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}},
},
},
expectedError: "",
},
// 新增场景:测试混合内容的子计划树
{
name: "PlanWithSubPlans_MixedContentTypes",
setupData: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 70}, Name: "Mixed Main Plan", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks})
db.Create(&models.Task{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1})
db.Create(&models.Plan{Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks})
db.Create(&models.Task{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1})
// 创建关联
db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1}) // Main -> Child with SubPlans
db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2}) // Main -> Child with Tasks
db.Create(&models.SubPlan{ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1}) // Child with SubPlans -> Grandchild
},
planID: 70,
expectedPlan: &models.Plan{
Model: gorm.Model{ID: 70},
Name: "Mixed Main Plan",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{
ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1,
ChildPlan: &models.Plan{
Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1, ChildPlan: &models.Plan{
Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}},
}},
},
},
},
{
ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2,
ChildPlan: &models.Plan{
Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}},
},
},
},
},
expectedError: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db := setupTestDB(t)
// 使用 defer 确保数据库连接在测试结束后关闭
sqlDB, _ := db.DB()
defer sqlDB.Close()
tc.setupData(db)
repo := repository.NewGormPlanRepository(db)
plan, err := repo.GetPlanByID(tc.planID)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
assert.Nil(t, plan)
} else {
assert.NoError(t, err)
assert.NotNil(t, plan)
// 在比较之前,清理实际结果和期望结果中所有不确定的、由数据库自动生成的字段
cleanPlanForComparison(plan)
cleanPlanForComparison(tc.expectedPlan)
assert.Equal(t, tc.expectedPlan, plan)
}
})
}
}
// cleanPlanForComparison 递归地重置 Plan 对象及其关联对象中由数据库自动生成的字段如ID和时间戳
// 以便在测试中断言它们与期望值相等。
func cleanPlanForComparison(p *models.Plan) {
if p == nil {
return
}
// 重置 Plan 自身的时间戳
p.CreatedAt = time.Time{}
p.UpdatedAt = time.Time{}
p.DeletedAt = gorm.DeletedAt{}
// 重置所有 Task 的自动生成字段
for i := range p.Tasks {
p.Tasks[i].ID = 0 // ID是自动生成的必须重置为0才能与期望值匹配
p.Tasks[i].CreatedAt = time.Time{}
p.Tasks[i].UpdatedAt = time.Time{}
p.Tasks[i].DeletedAt = gorm.DeletedAt{}
}
// 重置所有 SubPlan 的自动生成字段,并递归清理子计划
for i := range p.SubPlans {
p.SubPlans[i].ID = 0 // SubPlan 连接记录的ID也是自动生成的
p.SubPlans[i].CreatedAt = time.Time{}
p.SubPlans[i].UpdatedAt = time.Time{}
p.SubPlans[i].DeletedAt = gorm.DeletedAt{}
// 递归调用以清理嵌套的子计划
cleanPlanForComparison(p.SubPlans[i].ChildPlan)
}
}
// TestUpdatePlan_Validation 专注于测试 UpdatePlan 中前置检查逻辑的各种失败和成功场景。
func TestUpdatePlan_Validation(t *testing.T) {
// 定义Go测试中使用的计划实体
planA := &models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A", ContentType: models.PlanContentTypeSubPlans}
planB := &models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B", ContentType: models.PlanContentTypeSubPlans}
planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C", ContentType: models.PlanContentTypeSubPlans}
planD := &models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D", ContentType: models.PlanContentTypeTasks}
planNew := &models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} // ID为0的新计划
type testCase struct {
name string
setupDB func(db *gorm.DB)
buildInput func() *models.Plan // 修改为构建函数
expectedError string // 保持 string 类型
}
testCases := []testCase{
{
name: "成功-合法的菱形依赖树",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}})
db.Create(&models.Plan{Model: gorm.Model{ID: 4}})
},
buildInput: func() *models.Plan {
planD.ContentType = models.PlanContentTypeTasks
planB.ContentType = models.PlanContentTypeSubPlans
planB.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}}
planC.ContentType = models.PlanContentTypeSubPlans
planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}}
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{
{ChildPlanID: 2, ChildPlan: planB},
{ChildPlanID: 3, ChildPlan: planC},
}
return planA
},
expectedError: "", // 期望没有错误
},
{
name: "错误-根节点ID为零",
setupDB: func(db *gorm.DB) {},
buildInput: func() *models.Plan { return planNew },
expectedError: repository.ErrUpdateWithInvalidRoot.Error(),
},
{
name: "错误-子计划ID为零",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{{ChildPlan: planNew}}
return planA
},
expectedError: repository.ErrNewSubPlanInUpdate.Error(),
},
{
name: "错误-节点在数据库中不存在",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{
{ChildPlanID: 2, ChildPlan: planB},
{ChildPlanID: 3, ChildPlan: planC}, // C 不存在
}
return planA
},
expectedError: repository.ErrNodeDoesNotExist.Error(),
},
{
name: "错误-简单循环引用(A->B->A)",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}}
planB.ContentType = models.PlanContentTypeSubPlans
planB.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}}
return planA
},
expectedError: "检测到循环引用:计划 (ID: 1)",
},
{
name: "错误-复杂循环引用(A->B->C->A)",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}}
planB.ContentType = models.PlanContentTypeSubPlans
planB.SubPlans = []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}}
planC.ContentType = models.PlanContentTypeSubPlans
planC.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}}
return planA
},
expectedError: "检测到循环引用:计划 (ID: 1)",
},
{
name: "错误-自引用(A->A)",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}}
return planA
},
expectedError: "检测到循环引用:计划 (ID: 1)",
},
{
name: "错误-根节点内容混合",
setupDB: func(db *gorm.DB) {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}})
},
buildInput: func() *models.Plan {
planA.ContentType = models.PlanContentTypeSubPlans
planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}}
planA.Tasks = []models.Task{{Name: "A's Task"}}
return planA
},
expectedError: "不能同时包含任务和子计划",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 1. 为每个测试用例重置基础对象的状态
*planA = models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A"}
*planB = models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B"}
*planC = models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C"}
*planD = models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D"}
*planNew = models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"}
// 2. 设置数据库
db := setupTestDB(t)
sqlDB, _ := db.DB()
defer sqlDB.Close()
tc.setupDB(db)
// 3. 在对象重置后,构建本次测试需要的输入结构
input := tc.buildInput()
// 4. 执行测试
repo := repository.NewGormPlanRepository(db)
err := repo.UpdatePlan(input)
// 5. 断言结果
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}
// TestUpdatePlan_Reconciliation 专注于测试 UpdatePlan 成功执行后,数据库状态是否与预期一致。
func TestUpdatePlan_Reconciliation(t *testing.T) {
type testCase struct {
name string
setupDB func(db *gorm.DB) (rootPlanID uint)
buildInput func(db *gorm.DB) *models.Plan
verifyDB func(t *testing.T, db *gorm.DB, rootPlanID uint)
}
testCases := []testCase{
{
name: "任务协调-新增一个任务",
setupDB: func(db *gorm.DB) uint {
plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks}
db.Create(&plan)
db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 1})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan With Tasks",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{Model: gorm.Model{ID: 1}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1},
{Name: "New Task 2", ExecutionOrder: 2},
},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var finalPlan models.Plan
db.Preload("Tasks").First(&finalPlan, rootPlanID)
assert.Len(t, finalPlan.Tasks, 2)
assert.Equal(t, "New Task 2", finalPlan.Tasks[1].Name)
},
},
{
name: "任务协调-删除一个任务",
setupDB: func(db *gorm.DB) uint {
plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks}
db.Create(&plan)
db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1})
db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task to Delete", ExecutionOrder: 2})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan With Tasks",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1},
},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var tasks []models.Task
db.Where("plan_id = ?", rootPlanID).Find(&tasks)
assert.Len(t, tasks, 1)
var count int64
db.Model(&models.Task{}).Where("id = ?", 11).Count(&count)
assert.Equal(t, int64(0), count, "被删除的任务不应再存在")
},
},
{
name: "任务协调-更新并重排序任务",
setupDB: func(db *gorm.DB) uint {
plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks}
db.Create(&plan)
db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 1})
db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B", ExecutionOrder: 2})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B Updated", ExecutionOrder: 1},
{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 2},
},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var finalPlan models.Plan
db.Preload("Tasks", func(db *gorm.DB) *gorm.DB {
return db.Order("execution_order")
}).First(&finalPlan, rootPlanID)
assert.Len(t, finalPlan.Tasks, 2)
assert.Equal(t, "B Updated", finalPlan.Tasks[0].Name)
assert.Equal(t, uint(11), finalPlan.Tasks[0].ID)
},
},
{
name: "任务协调-混沌的同步操作(增删改重排)",
setupDB: func(db *gorm.DB) uint {
plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks}
db.Create(&plan)
db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Original)", ExecutionOrder: 1})
db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task 2 (To Be Deleted)", ExecutionOrder: 2})
db.Create(&models.Task{Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 3})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan",
ContentType: models.PlanContentTypeTasks,
Tasks: []models.Task{
// T4 (新) -> T3 (不变) -> T1 (更新)
{Name: "Task 4 (New)", ExecutionOrder: 1},
{Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 2},
{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Updated)", ExecutionOrder: 3},
},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var finalPlan models.Plan
db.Preload("Tasks", func(db *gorm.DB) *gorm.DB {
return db.Order("execution_order")
}).First(&finalPlan, rootPlanID)
// 验证最终数量
assert.Len(t, finalPlan.Tasks, 3)
// 验证被删除的 T2 不存在
var count int64
db.Model(&models.Task{}).Where("id = ?", 11).Count(&count)
assert.Equal(t, int64(0), count)
// 验证顺序和内容
assert.Equal(t, "Task 4 (New)", finalPlan.Tasks[0].Name)
assert.Equal(t, "Task 3 (Original)", finalPlan.Tasks[1].Name)
assert.Equal(t, "Task 1 (Updated)", finalPlan.Tasks[2].Name)
},
},
{
name: "子计划协调-新增一个关联",
setupDB: func(db *gorm.DB) uint {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Existing Child"})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Parent",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 2}},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var links []models.SubPlan
db.Where("parent_plan_id = ?", rootPlanID).Find(&links)
assert.Len(t, links, 1)
assert.Equal(t, uint(2), links[0].ChildPlanID)
},
},
{
name: "子计划协调-删除一个关联",
setupDB: func(db *gorm.DB) uint {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Child To Unlink"})
db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Parent",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var linkCount int64
db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount)
assert.Equal(t, int64(0), linkCount)
var planCount int64
db.Model(&models.Plan{}).Where("id = ?", 2).Count(&planCount)
assert.Equal(t, int64(1), planCount, "子计划本身不应被删除")
},
},
{
name: "类型转换-从任务切换到子计划",
setupDB: func(db *gorm.DB) uint {
plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks}
db.Create(&plan)
db.Create(&models.Task{PlanID: 1, Name: "Old Task"})
db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "New Child"})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 10}},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var taskCount int64
db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount)
assert.Equal(t, int64(0), taskCount, "旧任务应被清理")
var linkCount int64
db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount)
assert.Equal(t, int64(1), linkCount, "新关联应被创建")
},
},
{
name: "类型转换-从子计划切换到任务",
setupDB: func(db *gorm.DB) uint {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "Old Child"})
db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 10})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
return &models.Plan{
Model: gorm.Model{ID: 1},
Name: "Plan",
ContentType: models.PlanContentTypeTasks, // 类型变更
Tasks: []models.Task{{Name: "New Task"}},
}
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var linkCount int64
db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount)
assert.Equal(t, int64(0), linkCount, "旧的子计划关联应被清理")
var taskCount int64
db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount)
assert.Equal(t, int64(1), taskCount, "新任务应被创建")
},
},
{
name: "递归更新-深层节点的变更",
setupDB: func(db *gorm.DB) uint {
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeTasks})
db.Create(&models.SubPlan{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2, ExecutionOrder: 1})
db.Create(&models.SubPlan{Model: gorm.Model{ID: 102}, ParentPlanID: 2, ChildPlanID: 3, ExecutionOrder: 1})
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "C Updated", ContentType: models.PlanContentTypeTasks}
planB := &models.Plan{
Model: gorm.Model{ID: 2},
Name: "B",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{Model: gorm.Model{ID: 102}, ParentPlanID: 2, ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}},
}
planA := &models.Plan{
Model: gorm.Model{ID: 1},
Name: "A Updated",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}},
}
return planA
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
var finalA, finalC models.Plan
var finalLinkB models.SubPlan
db.First(&finalA, 1)
assert.Equal(t, "A Updated", finalA.Name)
db.First(&finalLinkB, 102)
assert.Equal(t, 2, finalLinkB.ExecutionOrder)
db.First(&finalC, 3)
assert.Equal(t, "C Updated", finalC.Name)
},
},
{
name: "递归更新-深层节点的类型转换(C从SubPlans变为Tasks)",
setupDB: func(db *gorm.DB) uint {
// 初始状态: A -> B -> C -> D
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeSubPlans}) // C的初始类型
db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D"})
db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2})
db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 3})
db.Create(&models.SubPlan{Model: gorm.Model{ID: 99}, ParentPlanID: 3, ChildPlanID: 4}) // C->D 的关联
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
// 更新操作: C的类型变为Tasks并增加一个新Task
planC := &models.Plan{
Model: gorm.Model{ID: 3},
Name: "C",
ContentType: models.PlanContentTypeTasks, // 类型变更
Tasks: []models.Task{{Name: "C's New Task"}},
}
planB := &models.Plan{
Model: gorm.Model{ID: 2},
Name: "B",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}},
}
planA := &models.Plan{
Model: gorm.Model{ID: 1},
Name: "A",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}},
}
return planA
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
// 1. 验证 C->D 的旧关联已被删除
var linkCount int64
db.Model(&models.SubPlan{}).Where("id = ?", 99).Count(&linkCount)
assert.Equal(t, int64(0), linkCount, "C的旧子计划关联应被清理")
// 2. 验证 C 的新Task已被创建
var task models.Task
err := db.Where("plan_id = ?", 3).First(&task).Error
assert.NoError(t, err, "C的新任务应该被创建")
assert.Equal(t, "C's New Task", task.Name)
// 3. 验证 D 计划本身依然存在
var planDCount int64
db.Model(&models.Plan{}).Where("id = ?", 4).Count(&planDCount)
assert.Equal(t, int64(1), planDCount, "计划D本身不应被删除")
},
}, {
name: "递归更新-中间层分支替换(A->B变为A->D)",
setupDB: func(db *gorm.DB) uint {
// 初始状态: A -> B -> C
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C"})
db.Create(&models.SubPlan{Model: gorm.Model{ID: 101}, ParentPlanID: 1, ChildPlanID: 2}) // A->B
db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 3}) // B->C
// 准备用于替换的分支: D -> E
db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 5}, Name: "E"})
db.Create(&models.SubPlan{ParentPlanID: 4, ChildPlanID: 5}) // D->E
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
// 更新操作: A的子计划从 B 替换为 D
planE := &models.Plan{Model: gorm.Model{ID: 5}, Name: "E Updated"} // 同时更新深层节点
planD := &models.Plan{
Model: gorm.Model{ID: 4},
Name: "D",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 5, ChildPlan: planE}},
}
planA := &models.Plan{
Model: gorm.Model{ID: 1},
Name: "A",
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}}, // 新关联 A->D
}
return planA
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
// 1. 验证 A->B 的旧关联已被删除
var linkCount int64
db.Model(&models.SubPlan{}).Where("id = ?", 101).Count(&linkCount)
assert.Equal(t, int64(0), linkCount, "A->B 的旧关联应被删除")
// 2. 验证 A->D 的新关联已创建
var newLink models.SubPlan
err := db.Where("parent_plan_id = ? AND child_plan_id = ?", 1, 4).First(&newLink).Error
assert.NoError(t, err, "A->D 的新关联应被创建")
// 3. 验证 B, C, D, E 计划本身都依然存在
var planCount int64
db.Model(&models.Plan{}).Where("id IN ?", []uint{2, 3, 4, 5}).Count(&planCount)
assert.Equal(t, int64(4), planCount, "所有被引用或解引用的计划本身都不应被删除")
// 4. 验证对新分支深层节点的递归更新已生效
var finalE models.Plan
db.First(&finalE, 5)
assert.Equal(t, "E Updated", finalE.Name)
},
},
{
name: "递归更新-菱形依赖下的冲突更新",
setupDB: func(db *gorm.DB) uint {
// 初始状态: A -> B -> D, A -> C -> D
db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "A", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "B", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 3}, Name: "C", ContentType: models.PlanContentTypeSubPlans})
db.Create(&models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Original)"}) // D的初始名字
// 创建关联
db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2, ExecutionOrder: 1}) // A->B
db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 3, ExecutionOrder: 2}) // A->C
db.Create(&models.SubPlan{ParentPlanID: 2, ChildPlanID: 4}) // B->D
db.Create(&models.SubPlan{ParentPlanID: 3, ChildPlanID: 4}) // C->D
return 1
},
buildInput: func(db *gorm.DB) *models.Plan {
// 在一次调用中,通过不同路径对 D 进行不同的更新
planD_fromB := &models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Updated from B)"}
planD_fromC := &models.Plan{Model: gorm.Model{ID: 4}, Name: "D (Updated from C)"}
planB := &models.Plan{
Model: gorm.Model{ID: 2},
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD_fromB}},
}
planC := &models.Plan{
Model: gorm.Model{ID: 3},
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD_fromC}},
}
planA := &models.Plan{
Model: gorm.Model{ID: 1},
ContentType: models.PlanContentTypeSubPlans,
SubPlans: []models.SubPlan{
{ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1},
{ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 在 B 之后
},
}
return planA
},
verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) {
// 验证D 的最终名字应该符合“最后一次更新获胜”的原则
// 由于 planC 在 planB 之后被处理D 的名字应该是 "D (Updated from C)"
var finalD models.Plan
db.First(&finalD, 4)
assert.Equal(t, "D (Updated from C)", finalD.Name, "共享下游节点的更新应以后一次执行为准")
// 确保所有关联依然存在
var linkCount int64
db.Model(&models.SubPlan{}).Where("child_plan_id = ?", 4).Count(&linkCount)
assert.Equal(t, int64(2), linkCount, "D 的两个上游关联都应继续存在")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db := setupTestDB(t)
sqlDB, _ := db.DB()
defer sqlDB.Close()
rootID := tc.setupDB(db)
input := tc.buildInput(db)
repo := repository.NewGormPlanRepository(db)
err := repo.UpdatePlan(input)
assert.NoError(t, err)
tc.verifyDB(t, db, rootID)
})
}
}