using FsCheck; using FsCheck.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MiAssessment.Admin.Business.Data; using MiAssessment.Admin.Business.Entities; using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Assessment; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Services; using Moq; using Xunit; namespace MiAssessment.Tests.Admin; /// /// Category 属性测试 /// 验证报告分类服务的正确性属性 /// public class CategoryPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 9: Tree Structure Correctness /// /// Property 9: 树形结构中所有子分类的 ParentId 必须引用存在的父分类 /// *For any* category tree query, all child categories SHALL have their ParentId /// referencing an existing parent category, and the tree structure SHALL correctly /// represent the parent-child relationships. /// /// **Validates: Requirements 6.1, 6.5** /// [Property(MaxTest = 100)] public bool TreeStructure_AllChildrenHaveValidParentId(PositiveInt seed) { // Arrange: 创建包含多级分类的数据库 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); // 创建顶级分类 var rootCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Root Category {seed.Get}", Code = $"ROOT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(rootCategory); dbContext.SaveChanges(); // 创建子分类 var childCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = rootCategory.Id, Name = $"Child Category {seed.Get}", Code = $"CHILD_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(childCategory); dbContext.SaveChanges(); // 创建孙子分类 var grandchildCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = childCategory.Id, Name = $"Grandchild Category {seed.Get}", Code = $"GRANDCHILD_{seed.Get}", CategoryType = 3, ScoreRule = 2, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(grandchildCategory); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取分类树 var tree = service.GetCategoryTreeAsync(assessmentTypeId).GetAwaiter().GetResult(); // Assert: 验证树形结构正确性 // 1. 应该有一个顶级分类 if (tree.Count != 1) return false; var root = tree[0]; if (root.ParentId != 0) return false; if (root.Id != rootCategory.Id) return false; // 2. 顶级分类应该有一个子分类 if (root.Children.Count != 1) return false; var child = root.Children[0]; if (child.ParentId != root.Id) return false; if (child.Id != childCategory.Id) return false; // 3. 子分类应该有一个孙子分类 if (child.Children.Count != 1) return false; var grandchild = child.Children[0]; if (grandchild.ParentId != child.Id) return false; if (grandchild.Id != grandchildCategory.Id) return false; return true; } /// /// Property 9: 创建子分类时 ParentId 必须引用存在的父分类 /// /// **Validates: Requirements 6.5** /// [Property(MaxTest = 100)] public bool TreeStructure_CreateChildWithInvalidParentId_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试创建引用不存在父分类的子分类 var request = new CreateCategoryRequest { AssessmentTypeId = assessmentTypeId, ParentId = 999999, // 不存在的父分类ID Name = $"Invalid Child {seed.Get}", Code = $"INVALID_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1 }; // Assert: 应该抛出 BusinessException try { service.CreateCategoryAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { return ex.Code == ErrorCodes.CategoryNotFound; } } /// /// Property 9: 创建子分类时 ParentId 引用存在的父分类应该成功 /// /// **Validates: Requirements 6.5** /// [Property(MaxTest = 100)] public bool TreeStructure_CreateChildWithValidParentId_ShouldSucceed(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); // 创建父分类 var parentCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Parent {seed.Get}", Code = $"PARENT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(parentCategory); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 创建引用存在父分类的子分类 var request = new CreateCategoryRequest { AssessmentTypeId = assessmentTypeId, ParentId = parentCategory.Id, Name = $"Child {seed.Get}", Code = $"CHILD_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 1 }; try { var childId = service.CreateCategoryAsync(request).GetAwaiter().GetResult(); var child = dbContext.ReportCategories.FirstOrDefault(c => c.Id == childId); return child != null && child.ParentId == parentCategory.Id; } catch (BusinessException) { return false; } } /// /// Property 9: 树形结构应该正确表示多级嵌套关系 /// /// **Validates: Requirements 6.1, 6.5** /// [Property(MaxTest = 50)] public bool TreeStructure_MultiLevelNesting_ShouldBeCorrect(PositiveInt seed, PositiveInt depth) { // 限制深度为1-5层 var actualDepth = (depth.Get % 5) + 1; // Arrange using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); // 创建多级分类 var categoryIds = new List(); long parentId = 0; for (int i = 0; i < actualDepth; i++) { var category = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = parentId, Name = $"Level {i} Category {seed.Get}", Code = $"L{i}_{seed.Get}", CategoryType = (i % 8) + 1, ScoreRule = (i % 2) + 1, Sort = i, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(category); dbContext.SaveChanges(); categoryIds.Add(category.Id); parentId = category.Id; } var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取分类树 var tree = service.GetCategoryTreeAsync(assessmentTypeId).GetAwaiter().GetResult(); // Assert: 验证树形结构深度正确 var currentLevel = tree; for (int i = 0; i < actualDepth; i++) { if (currentLevel.Count != 1) return false; var node = currentLevel[0]; if (node.Id != categoryIds[i]) return false; if (i < actualDepth - 1) { currentLevel = node.Children; } else { // 最后一级应该没有子分类 if (node.Children.Count != 0) return false; } } return true; } /// /// Property 9: 软删除的分类不应出现在树形结构中 /// /// **Validates: Requirements 6.1** /// [Property(MaxTest = 100)] public bool TreeStructure_DeletedCategoriesNotInTree(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); // 创建两个分类,一个正常,一个已删除 var normalCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Normal {seed.Get}", Code = $"NORMAL_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; var deletedCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Deleted {seed.Get}", Code = $"DELETED_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 2, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = true // 已软删除 }; dbContext.ReportCategories.AddRange(normalCategory, deletedCategory); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取分类树 var tree = service.GetCategoryTreeAsync(assessmentTypeId).GetAwaiter().GetResult(); // Assert: 只应该有一个分类(正常的那个) return tree.Count == 1 && tree[0].Id == normalCategory.Id; } #endregion #region Property 18: Category Deletion Constraint /// /// Property 18: 有子分类的分类不能删除 /// *For any* category with child categories, attempting to delete the category /// SHALL return an error and the category SHALL remain unchanged. /// /// **Validates: Requirements 6.6** /// [Property(MaxTest = 100)] public bool DeletionConstraint_CategoryWithChildren_ShouldFailToDelete(PositiveInt seed) { // Arrange: 创建父子分类 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var parentCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Parent {seed.Get}", Code = $"PARENT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(parentCategory); dbContext.SaveChanges(); var childCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = parentCategory.Id, Name = $"Child {seed.Get}", Code = $"CHILD_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(childCategory); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试删除有子分类的父分类 try { service.DeleteCategoryAsync(parentCategory.Id).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // Assert: 应该返回 CategoryHasChildren 错误 if (ex.Code != ErrorCodes.CategoryHasChildren) return false; // 验证父分类未被删除 var parent = dbContext.ReportCategories.FirstOrDefault(c => c.Id == parentCategory.Id); return parent != null && !parent.IsDeleted; } } /// /// Property 18: 没有子分类的分类可以删除 /// /// **Validates: Requirements 6.6** /// [Property(MaxTest = 100)] public bool DeletionConstraint_CategoryWithoutChildren_ShouldSucceedToDelete(PositiveInt seed) { // Arrange: 创建没有子分类的分类 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var category = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Leaf {seed.Get}", Code = $"LEAF_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(category); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 删除没有子分类的分类 try { var result = service.DeleteCategoryAsync(category.Id).GetAwaiter().GetResult(); // Assert: 应该删除成功(软删除) var deletedCategory = dbContext.ReportCategories.FirstOrDefault(c => c.Id == category.Id); return result && deletedCategory != null && deletedCategory.IsDeleted; } catch (BusinessException) { return false; } } /// /// Property 18: 删除子分类后,父分类可以被删除 /// /// **Validates: Requirements 6.6** /// [Property(MaxTest = 100)] public bool DeletionConstraint_AfterChildDeleted_ParentCanBeDeleted(PositiveInt seed) { // Arrange: 创建父子分类 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var parentCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Parent {seed.Get}", Code = $"PARENT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(parentCategory); dbContext.SaveChanges(); var childCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = parentCategory.Id, Name = $"Child {seed.Get}", Code = $"CHILD_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(childCategory); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 先删除子分类 try { service.DeleteCategoryAsync(childCategory.Id).GetAwaiter().GetResult(); } catch (BusinessException) { return false; } // 然后删除父分类 try { var result = service.DeleteCategoryAsync(parentCategory.Id).GetAwaiter().GetResult(); // Assert: 父分类应该被成功删除 var parent = dbContext.ReportCategories.FirstOrDefault(c => c.Id == parentCategory.Id); return result && parent != null && parent.IsDeleted; } catch (BusinessException) { return false; } } /// /// Property 18: 有多个子分类的分类不能删除 /// /// **Validates: Requirements 6.6** /// [Property(MaxTest = 50)] public bool DeletionConstraint_CategoryWithMultipleChildren_ShouldFailToDelete(PositiveInt seed, PositiveInt childCount) { // 限制子分类数量为1-5 var actualChildCount = (childCount.Get % 5) + 1; // Arrange: 创建父分类和多个子分类 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var parentCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Parent {seed.Get}", Code = $"PARENT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(parentCategory); dbContext.SaveChanges(); // 创建多个子分类 for (int i = 0; i < actualChildCount; i++) { var childCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = parentCategory.Id, Name = $"Child {i} {seed.Get}", Code = $"CHILD_{i}_{seed.Get}", CategoryType = (i % 8) + 1, ScoreRule = 1, Sort = i, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(childCategory); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试删除有多个子分类的父分类 try { service.DeleteCategoryAsync(parentCategory.Id).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // Assert: 应该返回 CategoryHasChildren 错误 if (ex.Code != ErrorCodes.CategoryHasChildren) return false; // 验证父分类未被删除 var parent = dbContext.ReportCategories.FirstOrDefault(c => c.Id == parentCategory.Id); return parent != null && !parent.IsDeleted; } } /// /// Property 18: 软删除的子分类不应阻止父分类删除 /// /// **Validates: Requirements 6.6** /// [Property(MaxTest = 100)] public bool DeletionConstraint_SoftDeletedChildrenDontBlockParentDeletion(PositiveInt seed) { // Arrange: 创建父分类和已软删除的子分类 using var dbContext = CreateDbContext(); var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get); var parentCategory = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = 0, Name = $"Parent {seed.Get}", Code = $"PARENT_{seed.Get}", CategoryType = 1, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.ReportCategories.Add(parentCategory); dbContext.SaveChanges(); var softDeletedChild = new ReportCategory { AssessmentTypeId = assessmentTypeId, ParentId = parentCategory.Id, Name = $"Deleted Child {seed.Get}", Code = $"DELETED_CHILD_{seed.Get}", CategoryType = 2, ScoreRule = 1, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = true // 已软删除 }; dbContext.ReportCategories.Add(softDeletedChild); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 删除父分类(子分类已软删除,不应阻止) try { var result = service.DeleteCategoryAsync(parentCategory.Id).GetAwaiter().GetResult(); // Assert: 父分类应该被成功删除 var parent = dbContext.ReportCategories.FirstOrDefault(c => c.Id == parentCategory.Id); return result && parent != null && parent.IsDeleted; } catch (BusinessException) { return false; } } #endregion #region 辅助方法 /// /// 创建内存数据库上下文 /// private AdminBusinessDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new AdminBusinessDbContext(options); } /// /// 创建测评类型并返回ID /// private long CreateAssessmentType(AdminBusinessDbContext dbContext, int seed) { var assessmentType = new AssessmentType { Name = $"Test Assessment {seed}", Code = $"TEST_{seed}_{Guid.NewGuid():N}".Substring(0, 20), Price = 99.99m, QuestionCount = 80, Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); return assessmentType.Id; } #endregion }