- 系统配置管理模块 (Config) - 内容管理模块 (Banner, Promotion) - 测评管理模块 (Type, Question, Category, Mapping, Conclusion) - 用户管理模块 (User) - 订单管理模块 (Order) - 规划师管理模块 (Planner) - 分销管理模块 (InviteCode, Commission, Withdrawal) - 数据统计仪表盘模块 (Dashboard) - 权限控制集成 - 服务注册配置 全部381个测试通过
411 lines
15 KiB
C#
411 lines
15 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.Services;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace MiAssessment.Tests.Admin;
|
||
|
||
/// <summary>
|
||
/// Order 属性测试
|
||
/// 验证订单服务的正确性属性
|
||
/// </summary>
|
||
public class OrderPropertyTests
|
||
{
|
||
private readonly Mock<ILogger<OrderService>> _mockLogger = new();
|
||
|
||
#region Property 12: Refund Status Transitions
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款操作将订单状态从已支付(2)转换为退款中(4)
|
||
/// *For any* refund operation on a paid order, the order Status SHALL transition
|
||
/// from 2 (paid) to 4 (refunding).
|
||
///
|
||
/// **Validates: Requirements 11.1, 11.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_PaidToRefunding(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已支付状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 发起退款
|
||
var result = service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult();
|
||
|
||
// Assert: 订单状态应该变为退款中(4)
|
||
var order = dbContext.Orders.Find(orderId);
|
||
return result && order != null && order.Status == 4;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款操作将订单状态从已完成(3)转换为退款中(4)
|
||
/// *For any* refund operation on a completed order, the order Status SHALL transition
|
||
/// from 3 (completed) to 4 (refunding).
|
||
///
|
||
/// **Validates: Requirements 11.1, 11.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_CompletedToRefunding(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已完成状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 3, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 发起退款
|
||
var result = service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult();
|
||
|
||
// Assert: 订单状态应该变为退款中(4)
|
||
var order = dbContext.Orders.Find(orderId);
|
||
return result && order != null && order.Status == 4;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 完成退款将订单状态从退款中(4)转换为已退款(5),并记录RefundTime
|
||
/// *For any* refund completion, the order Status SHALL transition from 4 (refunding)
|
||
/// to 5 (refunded) with RefundTime recorded.
|
||
///
|
||
/// **Validates: Requirements 11.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_RefundingToRefunded(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建退款中状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 4, payAmount: 100m);
|
||
// 设置退款金额(模拟已发起退款)
|
||
var order = dbContext.Orders.Find(orderId)!;
|
||
order.RefundAmount = 50m;
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 完成退款
|
||
var result = service.CompleteRefundAsync(orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 订单状态应该变为已退款(5),且RefundTime已记录
|
||
dbContext.Entry(order).Reload();
|
||
return result
|
||
&& order.Status == 5
|
||
&& order.RefundTime.HasValue;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款时记录RefundAmount
|
||
/// *For any* refund operation, the RefundAmount SHALL be recorded.
|
||
///
|
||
/// **Validates: Requirements 11.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_RefundAmountRecorded(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已支付状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var payAmount = (seed.Get % 1000) + 100m; // 100-1099
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: payAmount);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// 计算有效的退款金额(不超过实付金额)
|
||
var refundAmount = Math.Min((seed.Get % 100) + 1m, payAmount);
|
||
|
||
// Act: 发起退款
|
||
var result = service.RefundAsync(orderId, refundAmount, "测试退款").GetAwaiter().GetResult();
|
||
|
||
// Assert: RefundAmount 应该被正确记录
|
||
var order = dbContext.Orders.Find(orderId);
|
||
return result && order != null && order.RefundAmount == refundAmount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 非已支付/已完成状态的订单不能发起退款
|
||
/// *For any* order with Status not in [2, 3], refund operation SHALL fail.
|
||
///
|
||
/// **Validates: Requirements 11.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_InvalidStatusCannotRefund(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建非已支付/已完成状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
// 状态: 1待支付, 4退款中, 5已退款, 6已取消
|
||
var invalidStatuses = new[] { 1, 4, 5, 6 };
|
||
var status = invalidStatuses[seed.Get % invalidStatuses.Length];
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act & Assert: 发起退款应该抛出异常
|
||
try
|
||
{
|
||
service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult();
|
||
return false; // 不应该成功
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.OrderCannotRefund;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款金额不能超过实付金额
|
||
/// *For any* refund operation, the RefundAmount SHALL NOT exceed PayAmount.
|
||
///
|
||
/// **Validates: Requirements 11.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_RefundAmountNotExceedPayAmount(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已支付状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var payAmount = (seed.Get % 100) + 10m; // 10-109
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: payAmount);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// 退款金额超过实付金额
|
||
var refundAmount = payAmount + 1m;
|
||
|
||
// Act & Assert: 发起退款应该抛出异常
|
||
try
|
||
{
|
||
service.RefundAsync(orderId, refundAmount, "测试退款").GetAwaiter().GetResult();
|
||
return false; // 不应该成功
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.RefundAmountInvalid;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款金额必须大于0
|
||
/// *For any* refund operation, the RefundAmount SHALL be greater than 0.
|
||
///
|
||
/// **Validates: Requirements 11.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool RefundStatusTransition_RefundAmountMustBePositive(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已支付状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act & Assert: 退款金额为0或负数应该抛出异常
|
||
try
|
||
{
|
||
service.RefundAsync(orderId, 0m, "测试退款").GetAwaiter().GetResult();
|
||
return false; // 不应该成功
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.RefundAmountInvalid;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款失败时状态回滚
|
||
/// *For any* refund failure, the order Status SHALL revert and error reason SHALL be recorded.
|
||
///
|
||
/// **Validates: Requirements 11.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_FailedRefundRollback(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建退款中状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 4, payAmount: 100m);
|
||
var order = dbContext.Orders.Find(orderId)!;
|
||
order.RefundAmount = 50m;
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
var errorReason = $"退款失败原因_{seed.Get}";
|
||
|
||
// Act: 退款失败回滚
|
||
var result = service.RefundFailedAsync(orderId, errorReason).GetAwaiter().GetResult();
|
||
|
||
// Assert: 订单状态应该回滚到已支付(2),且记录失败原因
|
||
dbContext.Entry(order).Reload();
|
||
return result
|
||
&& order.Status == 2
|
||
&& order.RefundReason == errorReason;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 只有退款中状态的订单可以完成退款
|
||
/// *For any* order with Status not equal to 4, complete refund operation SHALL fail.
|
||
///
|
||
/// **Validates: Requirements 11.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_OnlyRefundingCanComplete(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建非退款中状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var invalidStatuses = new[] { 1, 2, 3, 5, 6 };
|
||
var status = invalidStatuses[seed.Get % invalidStatuses.Length];
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act & Assert: 完成退款应该抛出异常
|
||
try
|
||
{
|
||
service.CompleteRefundAsync(orderId).GetAwaiter().GetResult();
|
||
return false; // 不应该成功
|
||
}
|
||
catch (BusinessException ex)
|
||
{
|
||
return ex.Code == ErrorCodes.OrderCannotRefund;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: CanRefund 正确判断订单是否可退款
|
||
/// *For any* order, CanRefund SHALL return true only when Status is 2 or 3.
|
||
///
|
||
/// **Validates: Requirements 11.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_CanRefundCorrectness(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建不同状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var allStatuses = new[] { 1, 2, 3, 4, 5, 6 };
|
||
var status = allStatuses[seed.Get % allStatuses.Length];
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var canRefund = service.CanRefundAsync(orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 只有状态2或3可以退款
|
||
var expectedCanRefund = status == 2 || status == 3;
|
||
return canRefund == expectedCanRefund;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 12: 退款操作记录退款原因
|
||
/// *For any* refund operation with a reason, the RefundReason SHALL be recorded.
|
||
///
|
||
/// **Validates: Requirements 11.6**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool RefundStatusTransition_RefundReasonRecorded(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建已支付状态的订单
|
||
using var dbContext = CreateDbContext();
|
||
var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m);
|
||
var service = new OrderService(dbContext, _mockLogger.Object);
|
||
var refundReason = $"退款原因_{seed.Get}";
|
||
|
||
// Act: 发起退款
|
||
var result = service.RefundAsync(orderId, 50m, refundReason).GetAwaiter().GetResult();
|
||
|
||
// Assert: RefundReason 应该被正确记录
|
||
var order = dbContext.Orders.Find(orderId);
|
||
return result && order != null && order.RefundReason == refundReason;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 创建内存数据库上下文
|
||
/// </summary>
|
||
private AdminBusinessDbContext CreateDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
|
||
return new AdminBusinessDbContext(options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试订单
|
||
/// </summary>
|
||
/// <param name="dbContext">数据库上下文</param>
|
||
/// <param name="seed">随机种子</param>
|
||
/// <param name="status">订单状态</param>
|
||
/// <param name="payAmount">实付金额</param>
|
||
/// <returns>订单ID</returns>
|
||
private long CreateTestOrder(AdminBusinessDbContext dbContext, int seed, int status, decimal payAmount)
|
||
{
|
||
// 先创建用户
|
||
var user = new User
|
||
{
|
||
Uid = $"{seed % 1000000:D6}",
|
||
OpenId = $"openid_{seed}",
|
||
Phone = $"138{seed % 100000000:D8}",
|
||
Nickname = $"User_{seed}",
|
||
UserLevel = 1,
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Users.Add(user);
|
||
dbContext.SaveChanges();
|
||
|
||
// 创建订单
|
||
var order = new Order
|
||
{
|
||
OrderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{seed % 10000:D4}",
|
||
UserId = user.Id,
|
||
OrderType = (seed % 2) + 1, // 1或2
|
||
ProductId = seed % 100 + 1,
|
||
ProductName = $"测试商品_{seed}",
|
||
Amount = payAmount,
|
||
PayAmount = payAmount,
|
||
PayType = 1, // 微信支付
|
||
Status = status,
|
||
PayTime = status >= 2 ? DateTime.Now.AddMinutes(-seed % 60) : null,
|
||
CreateTime = DateTime.Now.AddDays(-seed % 30),
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
return order.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试订单(指定用户ID)
|
||
/// </summary>
|
||
private long CreateTestOrderWithUser(AdminBusinessDbContext dbContext, int seed, int status, decimal payAmount, long userId)
|
||
{
|
||
var order = new Order
|
||
{
|
||
OrderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{seed % 10000:D4}",
|
||
UserId = userId,
|
||
OrderType = (seed % 2) + 1,
|
||
ProductId = seed % 100 + 1,
|
||
ProductName = $"测试商品_{seed}",
|
||
Amount = payAmount,
|
||
PayAmount = payAmount,
|
||
PayType = 1,
|
||
Status = status,
|
||
PayTime = status >= 2 ? DateTime.Now.AddMinutes(-seed % 60) : null,
|
||
CreateTime = DateTime.Now.AddDays(-seed % 30),
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
return order.Id;
|
||
}
|
||
|
||
#endregion
|
||
}
|