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) } }