423 lines
14 KiB
C#
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;
|
|
});
|
|
}
|
|
}
|