mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/BannerPropertyTests.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

576 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}