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;
///
/// Order 属性测试
/// 验证订单服务的正确性属性
///
public class OrderPropertyTests
{
private readonly Mock> _mockLogger = new();
#region Property 12: Refund Status Transitions
///
/// 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**
///
[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;
}
///
/// 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**
///
[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;
}
///
/// 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**
///
[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;
}
///
/// Property 12: 退款时记录RefundAmount
/// *For any* refund operation, the RefundAmount SHALL be recorded.
///
/// **Validates: Requirements 11.3**
///
[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;
}
///
/// Property 12: 非已支付/已完成状态的订单不能发起退款
/// *For any* order with Status not in [2, 3], refund operation SHALL fail.
///
/// **Validates: Requirements 11.1**
///
[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;
}
}
///
/// Property 12: 退款金额不能超过实付金额
/// *For any* refund operation, the RefundAmount SHALL NOT exceed PayAmount.
///
/// **Validates: Requirements 11.3**
///
[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;
}
}
///
/// Property 12: 退款金额必须大于0
/// *For any* refund operation, the RefundAmount SHALL be greater than 0.
///
/// **Validates: Requirements 11.3**
///
[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;
}
}
///
/// Property 12: 退款失败时状态回滚
/// *For any* refund failure, the order Status SHALL revert and error reason SHALL be recorded.
///
/// **Validates: Requirements 11.4**
///
[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;
}
///
/// Property 12: 只有退款中状态的订单可以完成退款
/// *For any* order with Status not equal to 4, complete refund operation SHALL fail.
///
/// **Validates: Requirements 11.3**
///
[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;
}
}
///
/// Property 12: CanRefund 正确判断订单是否可退款
/// *For any* order, CanRefund SHALL return true only when Status is 2 or 3.
///
/// **Validates: Requirements 11.1**
///
[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;
}
///
/// Property 12: 退款操作记录退款原因
/// *For any* refund operation with a reason, the RefundReason SHALL be recorded.
///
/// **Validates: Requirements 11.6**
///
[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 辅助方法
///
/// 创建内存数据库上下文
///
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
///
/// 创建测试订单
///
/// 数据库上下文
/// 随机种子
/// 订单状态
/// 实付金额
/// 订单ID
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;
}
///
/// 创建测试订单(指定用户ID)
///
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
}