xiangyixiangqin/server/tests/XiangYi.Application.Tests/Services/OrderServicePropertyTests.cs
2026-01-02 18:00:49 +08:00

423 lines
14 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using Xunit;
using XiangYi.Application.Interfaces;
using XiangYi.Application.Services;
using XiangYi.Core.Enums;
namespace XiangYi.Application.Tests.Services;
/// <summary>
/// OrderService属性测试 - 订单创建完整性
/// </summary>
public class OrderCreateCompletenessPropertyTests
{
private static readonly System.Random _random = new();
/// <summary>
/// **Feature: backend-api, Property 16: 订单创建完整性**
/// **Validates: Requirements 7.1, 8.1**
///
/// *For any* 创建的订单, 应包含订单号、用户ID、金额、状态等必要字段
/// </summary>
[Property(MaxTest = 100)]
public Property Order_ShouldHaveAllRequiredFields()
{
var userIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary();
return Prop.ForAll(
userIdArb,
userId =>
{
var orderNo = "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + _random.Next(1000, 9999).ToString("D4");
var amount = 1299m;
var orderType = 1;
var productName = "不限时会员";
var isComplete = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, productName);
return isComplete;
});
}
/// <summary>
/// 订单创建 - 订单号不能为空
/// </summary>
[Property(MaxTest = 100)]
public Property Order_WithEmptyOrderNo_ShouldBeIncomplete()
{
var userIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary();
return Prop.ForAll(
userIdArb,
userId =>
{
var amount = 1299m;
var orderType = 1;
var productName = "不限时会员";
var isCompleteWithNull = OrderService.ValidateOrderCompleteness(
null, userId, amount, orderType, productName);
var isCompleteWithEmpty = OrderService.ValidateOrderCompleteness(
"", userId, amount, orderType, productName);
return !isCompleteWithNull && !isCompleteWithEmpty;
});
}
/// <summary>
/// 订单创建 - 用户ID必须大于0
/// </summary>
[Property(MaxTest = 100)]
public Property Order_WithInvalidUserId_ShouldBeIncomplete()
{
var invalidUserIdArb = Gen.Choose(-100, 0).Select(x => (long)x).ToArbitrary();
return Prop.ForAll(
invalidUserIdArb,
userId =>
{
var orderNo = "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + _random.Next(1000, 9999).ToString("D4");
var amount = 1299m;
var orderType = 1;
var productName = "不限时会员";
var isComplete = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, productName);
return !isComplete;
});
}
/// <summary>
/// 订单创建 - 金额必须大于0
/// </summary>
[Property(MaxTest = 100)]
public Property Order_WithInvalidAmount_ShouldBeIncomplete()
{
var invalidAmountArb = Gen.Choose(-100, 0).Select(x => (decimal)x).ToArbitrary();
return Prop.ForAll(
invalidAmountArb,
amount =>
{
var orderNo = "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + _random.Next(1000, 9999).ToString("D4");
var userId = 1L;
var orderType = 1;
var productName = "不限时会员";
var isComplete = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, productName);
return !isComplete;
});
}
/// <summary>
/// 订单创建 - 订单类型必须有效
/// </summary>
[Property(MaxTest = 100)]
public Property Order_WithInvalidOrderType_ShouldBeIncomplete()
{
var invalidOrderTypeArb = Gen.OneOf(
Gen.Choose(-10, 0),
Gen.Choose(3, 10)
).ToArbitrary();
return Prop.ForAll(
invalidOrderTypeArb,
orderType =>
{
var orderNo = "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + _random.Next(1000, 9999).ToString("D4");
var userId = 1L;
var amount = 1299m;
var productName = "不限时会员";
var isComplete = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, productName);
return !isComplete;
});
}
/// <summary>
/// 订单创建 - 商品名称不能为空
/// </summary>
[Property(MaxTest = 100)]
public Property Order_WithEmptyProductName_ShouldBeIncomplete()
{
var userIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary();
return Prop.ForAll(
userIdArb,
userId =>
{
var orderNo = "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + _random.Next(1000, 9999).ToString("D4");
var amount = 1299m;
var orderType = 1;
var isCompleteWithNull = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, null);
var isCompleteWithEmpty = OrderService.ValidateOrderCompleteness(
orderNo, userId, amount, orderType, "");
return !isCompleteWithNull && !isCompleteWithEmpty;
});
}
/// <summary>
/// 订单号生成 - 会员订单以M开头
/// </summary>
[Property(MaxTest = 100)]
public Property OrderNo_ForMembership_ShouldStartWithM()
{
var suffixArb = Gen.Choose(1000, 9999).ToArbitrary();
return Prop.ForAll(
suffixArb,
suffix =>
{
var timestamp = DateTime.Now;
var orderNo = OrderService.GenerateOrderNoForTest(
(int)OrderType.Membership, timestamp, suffix);
return orderNo.StartsWith("M");
});
}
/// <summary>
/// 订单号生成 - 实名认证订单以R开头
/// </summary>
[Property(MaxTest = 100)]
public Property OrderNo_ForRealName_ShouldStartWithR()
{
var suffixArb = Gen.Choose(1000, 9999).ToArbitrary();
return Prop.ForAll(
suffixArb,
suffix =>
{
var timestamp = DateTime.Now;
var orderNo = OrderService.GenerateOrderNoForTest(
(int)OrderType.RealName, timestamp, suffix);
return orderNo.StartsWith("R");
});
}
/// <summary>
/// 订单号生成 - 长度应该足够
/// </summary>
[Property(MaxTest = 100)]
public Property OrderNo_ShouldHaveSufficientLength()
{
var orderTypeArb = Gen.Choose(1, 2).ToArbitrary();
return Prop.ForAll(
orderTypeArb,
orderType =>
{
var timestamp = DateTime.Now;
var suffix = _random.Next(1000, 9999);
var orderNo = OrderService.GenerateOrderNoForTest(orderType, timestamp, suffix);
return orderNo.Length >= 10;
});
}
}
/// <summary>
/// OrderService属性测试 - 支付回调幂等性
/// </summary>
public class PayCallbackIdempotencyPropertyTests
{
private static readonly System.Random _random = new();
/// <summary>
/// **Feature: backend-api, Property 17: 支付回调幂等性**
/// **Validates: Requirements 7.2**
///
/// *For any* 微信支付回调, 重复调用不应重复开通会员或更新订单状态
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_ShouldBeIdempotent()
{
var statusArb = Gen.Choose(1, 4).ToArbitrary();
return Prop.ForAll(
statusArb,
currentStatus =>
{
var tradeStates = new[] { "SUCCESS", "NOTPAY", "CLOSED", "REFUND" };
foreach (var tradeState in tradeStates)
{
var statusAfterFirst = OrderService.CalculateStatusAfterCallback(currentStatus, tradeState);
var statusAfterSecond = OrderService.CalculateStatusAfterCallback(statusAfterFirst, tradeState);
if (statusAfterFirst != statusAfterSecond)
return false;
}
return true;
});
}
/// <summary>
/// 支付回调 - 只有待支付状态才处理
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_OnlyPendingStatus_ShouldProcess()
{
var nonPendingStatusArb = Gen.Choose(2, 4).ToArbitrary();
return Prop.ForAll(
nonPendingStatusArb,
currentStatus =>
{
var shouldProcess = OrderService.ShouldProcessPayCallback(currentStatus, "SUCCESS");
var statusAfter = OrderService.CalculateStatusAfterCallback(currentStatus, "SUCCESS");
return !shouldProcess && statusAfter == currentStatus;
});
}
/// <summary>
/// 支付回调 - 待支付状态且支付成功才处理
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_PendingAndSuccess_ShouldProcess()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
_ =>
{
var pendingStatus = 1;
var successTradeState = "SUCCESS";
var shouldProcess = OrderService.ShouldProcessPayCallback(pendingStatus, successTradeState);
var statusAfter = OrderService.CalculateStatusAfterCallback(pendingStatus, successTradeState);
return shouldProcess && statusAfter == 2;
});
}
/// <summary>
/// 支付回调 - 非成功交易状态不处理
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_NonSuccessTradeState_ShouldNotProcess()
{
var nonSuccessTradeStateArb = Gen.Elements("NOTPAY", "CLOSED", "REFUND", "PAYERROR").ToArbitrary();
return Prop.ForAll(
nonSuccessTradeStateArb,
tradeState =>
{
var pendingStatus = 1;
var shouldProcess = OrderService.ShouldProcessPayCallback(pendingStatus, tradeState);
var statusAfter = OrderService.CalculateStatusAfterCallback(pendingStatus, tradeState);
return !shouldProcess && statusAfter == pendingStatus;
});
}
/// <summary>
/// 支付回调 - 已支付订单重复回调不改变状态
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_AlreadyPaid_ShouldNotChangeStatus()
{
var tradeStateArb = Gen.Elements("SUCCESS", "NOTPAY", "CLOSED", "REFUND").ToArbitrary();
return Prop.ForAll(
tradeStateArb,
tradeState =>
{
var paidStatus = 2;
var shouldProcess = OrderService.ShouldProcessPayCallback(paidStatus, tradeState);
var statusAfter = OrderService.CalculateStatusAfterCallback(paidStatus, tradeState);
return !shouldProcess && statusAfter == paidStatus;
});
}
/// <summary>
/// 支付回调 - 已取消订单不处理
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_CancelledOrder_ShouldNotProcess()
{
var tradeStateArb = Gen.Elements("SUCCESS", "NOTPAY", "CLOSED", "REFUND").ToArbitrary();
return Prop.ForAll(
tradeStateArb,
tradeState =>
{
var cancelledStatus = 3;
var shouldProcess = OrderService.ShouldProcessPayCallback(cancelledStatus, tradeState);
var statusAfter = OrderService.CalculateStatusAfterCallback(cancelledStatus, tradeState);
return !shouldProcess && statusAfter == cancelledStatus;
});
}
/// <summary>
/// 支付回调 - 已退款订单不处理
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_RefundedOrder_ShouldNotProcess()
{
var tradeStateArb = Gen.Elements("SUCCESS", "NOTPAY", "CLOSED", "REFUND").ToArbitrary();
return Prop.ForAll(
tradeStateArb,
tradeState =>
{
var refundedStatus = 4;
var shouldProcess = OrderService.ShouldProcessPayCallback(refundedStatus, tradeState);
var statusAfter = OrderService.CalculateStatusAfterCallback(refundedStatus, tradeState);
return !shouldProcess && statusAfter == refundedStatus;
});
}
/// <summary>
/// 支付回调 - 多次重复回调状态一致
/// </summary>
[Property(MaxTest = 100)]
public Property PayCallback_MultipleCallbacks_ShouldBeConsistent()
{
var statusArb = Gen.Choose(1, 4).ToArbitrary();
return Prop.ForAll(
statusArb,
initialStatus =>
{
var tradeStates = new[] { "SUCCESS", "NOTPAY", "CLOSED", "REFUND" };
foreach (var tradeState in tradeStates)
{
var currentStatus = initialStatus;
var repeatCount = _random.Next(2, 10);
for (int i = 0; i < repeatCount; i++)
{
currentStatus = OrderService.CalculateStatusAfterCallback(currentStatus, tradeState);
}
var expectedStatus = OrderService.CalculateStatusAfterCallback(initialStatus, tradeState);
if (currentStatus != expectedStatus)
return false;
}
return true;
});
}
}