mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/CategoryPropertyTests.cs
zpc 6bf2ea595c feat(admin-business): 完成后台管理系统全部业务模块
- 系统配置管理模块 (Config)
- 内容管理模块 (Banner, Promotion)
- 测评管理模块 (Type, Question, Category, Mapping, Conclusion)
- 用户管理模块 (User)
- 订单管理模块 (Order)
- 规划师管理模块 (Planner)
- 分销管理模块 (InviteCode, Commission, Withdrawal)
- 数据统计仪表盘模块 (Dashboard)
- 权限控制集成
- 服务注册配置

全部381个测试通过
2026-02-03 20:50:51 +08:00

690 lines
22 KiB
C#

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;
/// <summary>
/// Category 属性测试
/// 验证报告分类服务的正确性属性
/// </summary>
public class CategoryPropertyTests
{
private readonly Mock<ILogger<AssessmentService>> _mockLogger = new();
#region Property 9: Tree Structure Correctness
/// <summary>
/// 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**
/// </summary>
[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;
}
/// <summary>
/// Property 9: 创建子分类时 ParentId 必须引用存在的父分类
///
/// **Validates: Requirements 6.5**
/// </summary>
[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;
}
}
/// <summary>
/// Property 9: 创建子分类时 ParentId 引用存在的父分类应该成功
///
/// **Validates: Requirements 6.5**
/// </summary>
[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;
}
}
/// <summary>
/// Property 9: 树形结构应该正确表示多级嵌套关系
///
/// **Validates: Requirements 6.1, 6.5**
/// </summary>
[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>();
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;
}
/// <summary>
/// Property 9: 软删除的分类不应出现在树形结构中
///
/// **Validates: Requirements 6.1**
/// </summary>
[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
/// <summary>
/// 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**
/// </summary>
[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;
}
}
/// <summary>
/// Property 18: 没有子分类的分类可以删除
///
/// **Validates: Requirements 6.6**
/// </summary>
[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;
}
}
/// <summary>
/// Property 18: 删除子分类后,父分类可以被删除
///
/// **Validates: Requirements 6.6**
/// </summary>
[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;
}
}
/// <summary>
/// Property 18: 有多个子分类的分类不能删除
///
/// **Validates: Requirements 6.6**
/// </summary>
[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;
}
}
/// <summary>
/// Property 18: 软删除的子分类不应阻止父分类删除
///
/// **Validates: Requirements 6.6**
/// </summary>
[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
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
/// <summary>
/// 创建测评类型并返回ID
/// </summary>
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
}