798 lines
28 KiB
Go
798 lines
28 KiB
Go
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 {
|
||
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: "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)
|
||
},
|
||
},
|
||
}
|
||
|
||
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)
|
||
})
|
||
}
|
||
}
|