- 系统配置管理模块 (Config) - 内容管理模块 (Banner, Promotion) - 测评管理模块 (Type, Question, Category, Mapping, Conclusion) - 用户管理模块 (User) - 订单管理模块 (Order) - 规划师管理模块 (Planner) - 分销管理模块 (InviteCode, Commission, Withdrawal) - 数据统计仪表盘模块 (Dashboard) - 权限控制集成 - 服务注册配置 全部381个测试通过
576 lines
19 KiB
C#
576 lines
19 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.Common;
|
||
using MiAssessment.Admin.Business.Models.Content;
|
||
using MiAssessment.Admin.Business.Services;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace MiAssessment.Tests.Admin;
|
||
|
||
/// <summary>
|
||
/// Banner 属性测试
|
||
/// 验证轮播图服务的正确性属性
|
||
/// </summary>
|
||
public class BannerPropertyTests
|
||
{
|
||
private readonly Mock<ILogger<ContentService>> _mockLogger = new();
|
||
|
||
#region Property 1: Soft Delete Behavior
|
||
|
||
/// <summary>
|
||
/// Property 1: 软删除后实体仍存在于数据库中
|
||
/// *For any* entity that supports soft delete, when a delete operation is performed,
|
||
/// the entity's IsDeleted field SHALL be set to true and the entity SHALL remain in the database.
|
||
///
|
||
/// **Validates: Requirements 2.7**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SoftDeleteBehavior_EntityRemainsInDatabase(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建包含轮播图的数据库
|
||
using var dbContext = CreateDbContext();
|
||
var bannerId = CreateTestBanner(dbContext, seed.Get);
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 执行删除操作
|
||
var result = service.DeleteBannerAsync(bannerId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 实体应该仍然存在于数据库中,且 IsDeleted = true
|
||
var deletedBanner = dbContext.Banners.FirstOrDefault(b => b.Id == bannerId);
|
||
|
||
return result
|
||
&& deletedBanner != null
|
||
&& deletedBanner.IsDeleted;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1: 软删除后的实体不应出现在列表查询中
|
||
/// *For any* deleted entity, it SHALL NOT appear in list queries.
|
||
///
|
||
/// **Validates: Requirements 2.7**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SoftDeleteBehavior_DeletedEntityNotInListQuery(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建多个轮播图
|
||
using var dbContext = CreateDbContext();
|
||
var bannerId1 = CreateTestBanner(dbContext, seed.Get);
|
||
var bannerId2 = CreateTestBanner(dbContext, seed.Get + 1000);
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 删除其中一个轮播图
|
||
service.DeleteBannerAsync(bannerId1).GetAwaiter().GetResult();
|
||
|
||
// 查询列表
|
||
var request = new BannerQueryRequest { Page = 1, PageSize = 100 };
|
||
var result = service.GetBannerListAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 删除的轮播图不应出现在列表中
|
||
var deletedBannerInList = result.List.Any(b => b.Id == bannerId1);
|
||
var activeBannerInList = result.List.Any(b => b.Id == bannerId2);
|
||
|
||
return !deletedBannerInList && activeBannerInList;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1: 软删除后 UpdateTime 应该被更新
|
||
///
|
||
/// **Validates: Requirements 2.7**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool SoftDeleteBehavior_UpdateTimeIsSet(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var originalUpdateTime = DateTime.Now.AddDays(-1);
|
||
|
||
var banner = new Banner
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = 0,
|
||
Sort = seed.Get % 100,
|
||
Status = 1,
|
||
CreateTime = originalUpdateTime,
|
||
UpdateTime = originalUpdateTime,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Banners.Add(banner);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
var beforeDelete = DateTime.Now;
|
||
|
||
// Act: 执行删除操作
|
||
service.DeleteBannerAsync(banner.Id).GetAwaiter().GetResult();
|
||
|
||
var afterDelete = DateTime.Now;
|
||
|
||
// Assert: UpdateTime 应该在删除前后的时间范围内
|
||
var deletedBanner = dbContext.Banners.First(b => b.Id == banner.Id);
|
||
|
||
return deletedBanner.UpdateTime >= beforeDelete
|
||
&& deletedBanner.UpdateTime <= afterDelete
|
||
&& deletedBanner.UpdateTime > originalUpdateTime;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 4: Sorting Correctness
|
||
|
||
/// <summary>
|
||
/// Property 4: 列表按 Sort 字段降序排列
|
||
/// *For any* list query with a Sort field, the returned items SHALL be ordered
|
||
/// by the Sort field in descending order.
|
||
///
|
||
/// **Validates: Requirements 2.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SortingCorrectness_ListOrderedBySortDescending(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建多个具有不同 Sort 值的轮播图
|
||
using var dbContext = CreateDbContext();
|
||
var sortValues = GenerateSortValues(seed.Get, 5);
|
||
|
||
foreach (var sortValue in sortValues)
|
||
{
|
||
var banner = new Banner
|
||
{
|
||
Title = $"Banner Sort {sortValue}",
|
||
ImageUrl = $"https://example.com/image_{sortValue}.jpg",
|
||
LinkType = 0,
|
||
Sort = sortValue,
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Banners.Add(banner);
|
||
}
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 查询列表
|
||
var request = new BannerQueryRequest { Page = 1, PageSize = 100 };
|
||
var result = service.GetBannerListAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 列表应该按 Sort 降序排列
|
||
if (result.List.Count < 2) return true;
|
||
|
||
for (int i = 0; i < result.List.Count - 1; i++)
|
||
{
|
||
if (result.List[i].Sort < result.List[i + 1].Sort)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 相同 Sort 值时按 CreateTime 降序排列
|
||
///
|
||
/// **Validates: Requirements 2.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool SortingCorrectness_SameSortOrderedByCreateTimeDescending(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建多个具有相同 Sort 值但不同 CreateTime 的轮播图
|
||
using var dbContext = CreateDbContext();
|
||
var sameSort = seed.Get % 100;
|
||
var baseTime = DateTime.Now.AddDays(-10);
|
||
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var banner = new Banner
|
||
{
|
||
Title = $"Banner {i}",
|
||
ImageUrl = $"https://example.com/image_{i}.jpg",
|
||
LinkType = 0,
|
||
Sort = sameSort,
|
||
Status = 1,
|
||
CreateTime = baseTime.AddDays(i), // 不同的创建时间
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Banners.Add(banner);
|
||
}
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 查询列表
|
||
var request = new BannerQueryRequest { Page = 1, PageSize = 100 };
|
||
var result = service.GetBannerListAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 相同 Sort 值时应该按 CreateTime 降序排列
|
||
var sameSortBanners = result.List.Where(b => b.Sort == sameSort).ToList();
|
||
if (sameSortBanners.Count < 2) return true;
|
||
|
||
for (int i = 0; i < sameSortBanners.Count - 1; i++)
|
||
{
|
||
if (sameSortBanners[i].CreateTime < sameSortBanners[i + 1].CreateTime)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 排序更新后列表顺序应该正确
|
||
///
|
||
/// **Validates: Requirements 2.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool SortingCorrectness_AfterSortUpdate_ListOrderCorrect(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建轮播图
|
||
using var dbContext = CreateDbContext();
|
||
var banner1 = CreateTestBannerWithSort(dbContext, seed.Get, 10);
|
||
var banner2 = CreateTestBannerWithSort(dbContext, seed.Get + 1, 20);
|
||
var banner3 = CreateTestBannerWithSort(dbContext, seed.Get + 2, 30);
|
||
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 更新排序,将 banner1 的 Sort 改为最大
|
||
var sortItems = new List<SortItem>
|
||
{
|
||
new SortItem { Id = banner1, Sort = 100 }
|
||
};
|
||
service.UpdateBannerSortAsync(sortItems).GetAwaiter().GetResult();
|
||
|
||
// 查询列表
|
||
var request = new BannerQueryRequest { Page = 1, PageSize = 100 };
|
||
var result = service.GetBannerListAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: banner1 应该排在第一位
|
||
return result.List.Count > 0 && result.List[0].Id == banner1;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 7: Banner Link Type Validation
|
||
|
||
/// <summary>
|
||
/// Property 7: LinkType 1 或 2 时 LinkUrl 必填
|
||
/// *For any* Banner with LinkType 1 or 2, the LinkUrl SHALL be non-empty.
|
||
///
|
||
/// **Validates: Requirements 2.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool LinkTypeValidation_LinkUrlRequiredForType1And2(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// 测试 LinkType 1 (内部页面) 和 2 (外部链接)
|
||
var linkType = (seed.Get % 2) + 1; // 1 或 2
|
||
|
||
var request = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = linkType,
|
||
LinkUrl = null, // 空的 LinkUrl
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
// Act & Assert: 应该抛出 BusinessException
|
||
try
|
||
{
|
||
service.CreateBannerAsync(request).GetAwaiter().GetResult();
|
||
return false; // 应该抛出异常
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.BannerLinkUrlRequired;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 7: LinkType 1 或 2 时提供有效 LinkUrl 应该成功
|
||
///
|
||
/// **Validates: Requirements 2.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool LinkTypeValidation_ValidLinkUrlForType1And2_Success(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
var linkType = (seed.Get % 2) + 1; // 1 或 2
|
||
|
||
var request = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = linkType,
|
||
LinkUrl = $"https://example.com/page_{seed.Get}", // 有效的 LinkUrl
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
// Act
|
||
var bannerId = service.CreateBannerAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该创建成功
|
||
var banner = dbContext.Banners.FirstOrDefault(b => b.Id == bannerId);
|
||
return banner != null && banner.LinkType == linkType && !string.IsNullOrEmpty(banner.LinkUrl);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 7: LinkType 3 时 LinkUrl 和 AppId 都必填
|
||
/// *For any* Banner with LinkType 3, both LinkUrl and AppId SHALL be non-empty.
|
||
///
|
||
/// **Validates: Requirements 2.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool LinkTypeValidation_LinkUrlAndAppIdRequiredForType3(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// 测试场景:LinkUrl 为空
|
||
var request1 = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = 3, // 小程序
|
||
LinkUrl = null, // 空的 LinkUrl
|
||
AppId = "wx1234567890",
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
bool linkUrlValidationFailed = false;
|
||
try
|
||
{
|
||
service.CreateBannerAsync(request1).GetAwaiter().GetResult();
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
linkUrlValidationFailed = ex.Code == ErrorCodes.BannerLinkUrlRequired;
|
||
}
|
||
|
||
// 测试场景:AppId 为空
|
||
var request2 = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get + 1}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get + 1}.jpg",
|
||
LinkType = 3, // 小程序
|
||
LinkUrl = "/pages/index/index",
|
||
AppId = null, // 空的 AppId
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
bool appIdValidationFailed = false;
|
||
try
|
||
{
|
||
service.CreateBannerAsync(request2).GetAwaiter().GetResult();
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
appIdValidationFailed = ex.Code == ErrorCodes.BannerLinkUrlRequired;
|
||
}
|
||
|
||
return linkUrlValidationFailed && appIdValidationFailed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 7: LinkType 3 时提供有效 LinkUrl 和 AppId 应该成功
|
||
///
|
||
/// **Validates: Requirements 2.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool LinkTypeValidation_ValidLinkUrlAndAppIdForType3_Success(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
var request = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = 3, // 小程序
|
||
LinkUrl = "/pages/index/index",
|
||
AppId = $"wx{seed.Get:D10}", // 有效的 AppId
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
// Act
|
||
var bannerId = service.CreateBannerAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该创建成功
|
||
var banner = dbContext.Banners.FirstOrDefault(b => b.Id == bannerId);
|
||
return banner != null
|
||
&& banner.LinkType == 3
|
||
&& !string.IsNullOrEmpty(banner.LinkUrl)
|
||
&& !string.IsNullOrEmpty(banner.AppId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 7: LinkType 0 时不需要 LinkUrl 和 AppId
|
||
/// *For any* Banner with LinkType 0, the LinkUrl and AppId are optional.
|
||
///
|
||
/// **Validates: Requirements 2.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool LinkTypeValidation_NoValidationForType0(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
var request = new CreateBannerRequest
|
||
{
|
||
Title = $"Test Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/image_{seed.Get}.jpg",
|
||
LinkType = 0, // 无跳转
|
||
LinkUrl = null, // 空的 LinkUrl
|
||
AppId = null, // 空的 AppId
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
// Act
|
||
try
|
||
{
|
||
var bannerId = service.CreateBannerAsync(request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该创建成功
|
||
var banner = dbContext.Banners.FirstOrDefault(b => b.Id == bannerId);
|
||
return banner != null && banner.LinkType == 0;
|
||
}
|
||
catch (BusinessException)
|
||
{
|
||
return false; // 不应该抛出异常
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 7: 更新时也应验证 LinkType
|
||
///
|
||
/// **Validates: Requirements 2.3, 2.4, 2.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool LinkTypeValidation_UpdateAlsoValidates(PositiveInt seed)
|
||
{
|
||
// Arrange: 先创建一个有效的轮播图
|
||
using var dbContext = CreateDbContext();
|
||
var bannerId = CreateTestBanner(dbContext, seed.Get);
|
||
var service = new ContentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 尝试更新为 LinkType 1 但不提供 LinkUrl
|
||
var updateRequest = new UpdateBannerRequest
|
||
{
|
||
Id = bannerId,
|
||
Title = $"Updated Banner {seed.Get}",
|
||
ImageUrl = $"https://example.com/updated_{seed.Get}.jpg",
|
||
LinkType = 1, // 内部页面
|
||
LinkUrl = null, // 空的 LinkUrl
|
||
Sort = seed.Get % 100,
|
||
Status = 1
|
||
};
|
||
|
||
// Assert: 应该抛出 BusinessException
|
||
try
|
||
{
|
||
service.UpdateBannerAsync(updateRequest).GetAwaiter().GetResult();
|
||
return false;
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.BannerLinkUrlRequired;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 创建内存数据库上下文
|
||
/// </summary>
|
||
private AdminBusinessDbContext CreateDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
|
||
return new AdminBusinessDbContext(options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试轮播图
|
||
/// </summary>
|
||
private long CreateTestBanner(AdminBusinessDbContext dbContext, int seed)
|
||
{
|
||
var banner = new Banner
|
||
{
|
||
Title = $"Test Banner {seed}",
|
||
ImageUrl = $"https://example.com/image_{seed}.jpg",
|
||
LinkType = 0,
|
||
Sort = seed % 100,
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Banners.Add(banner);
|
||
dbContext.SaveChanges();
|
||
return banner.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建指定 Sort 值的测试轮播图
|
||
/// </summary>
|
||
private long CreateTestBannerWithSort(AdminBusinessDbContext dbContext, int seed, int sort)
|
||
{
|
||
var banner = new Banner
|
||
{
|
||
Title = $"Test Banner {seed}",
|
||
ImageUrl = $"https://example.com/image_{seed}.jpg",
|
||
LinkType = 0,
|
||
Sort = sort,
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Banners.Add(banner);
|
||
dbContext.SaveChanges();
|
||
return banner.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成不同的 Sort 值列表
|
||
/// </summary>
|
||
private List<int> GenerateSortValues(int seed, int count)
|
||
{
|
||
var values = new List<int>();
|
||
var random = new Random(seed);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
values.Add(random.Next(1, 1000));
|
||
}
|
||
return values;
|
||
}
|
||
|
||
#endregion
|
||
}
|