mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Core/PaymentRewardDispatcherPropertyTests.cs
2026-02-03 14:25:01 +08:00

541 lines
20 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Services;
using MiAssessment.Model.Entities;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Core;
/// <summary>
/// PaymentRewardDispatcher 属性测试
/// Feature: framework-template
///
/// Property 5: Reward Handler Dispatch
/// *For any* payment order with a specific order_type:
/// - The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
/// - If a handler is found, its ProcessRewardAsync method SHALL be called with the order
/// - The handler's RewardResult SHALL determine the order's reward_status and reward_data
///
/// **Validates: Requirements 6.2, 6.3**
/// </summary>
public class PaymentRewardDispatcherPropertyTests
{
private readonly Mock<ILogger<PaymentRewardDispatcher>> _mockLogger = new();
#region Property 5: Reward Handler Dispatch
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.1: Handler found for order type → handler's ProcessRewardAsync is called
/// If a handler is found, its ProcessRewardAsync method SHALL be called with the order
///
/// **Validates: Requirements 6.2, 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithMatchingHandler_ShouldCallProcessRewardAsync(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var handlerCalled = false;
PaymentOrder? receivedOrder = null;
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback<PaymentOrder>(order =>
{
handlerCalled = true;
receivedOrder = order;
})
.ReturnsAsync(RewardResult.Ok());
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Handler should be called with the correct order
return handlerCalled && receivedOrder != null && receivedOrder.OrderNo == paymentOrder.OrderNo;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.2: Handler not found → returns success with no reward data
/// If no handler is found for the order type, the system should return success (not failure)
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithNoMatchingHandler_ShouldReturnSuccessWithNoRewardData(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var differentOrderType = $"different_{orderType}";
// Create a handler for a different order type
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(differentOrderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok("should_not_be_called"));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return success with no reward data
return result.Success && result.RewardData == null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.3: Handler returns success → result contains reward data
/// The handler's RewardResult SHALL determine the order's reward_data
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithSuccessfulHandler_ShouldReturnRewardData(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var expectedRewardData = $"{{\"diamonds\":{seed.Get % 1000 + 100},\"bonus\":{seed.Get % 50}}}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should contain the reward data from handler
return result.Success && result.RewardData == expectedRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.4: Handler returns failure → result contains error message
/// The handler's RewardResult SHALL determine the order's reward_status
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithFailedHandler_ShouldReturnErrorMessage(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var expectedErrorMessage = $"Reward processing failed for seed {seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Fail(expectedErrorMessage));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should contain the error message from handler
return !result.Success && result.Message == expectedErrorMessage;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.5: Handler throws exception → result contains error message
/// If the handler throws an exception, the dispatcher should catch it and return failure
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithExceptionThrowingHandler_ShouldReturnErrorMessage(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var exceptionMessage = $"Unexpected error for seed {seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ThrowsAsync(new InvalidOperationException(exceptionMessage));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should be failure and contain error message
return !result.Success && result.Message != null && result.Message.Contains(exceptionMessage);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.6: Multiple handlers registered → correct handler is selected
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithMultipleHandlers_ShouldSelectCorrectHandler(PositiveInt seed)
{
// Arrange
var targetOrderType = GenerateValidOrderType(seed.Get);
var otherOrderType1 = $"other1_{seed.Get}";
var otherOrderType2 = $"other2_{seed.Get}";
var targetRewardData = $"{{\"target_reward\":{seed.Get}}}";
var targetHandlerCalled = false;
var otherHandler1Called = false;
var otherHandler2Called = false;
// Create target handler
var targetHandler = new Mock<IPaymentRewardHandler>();
targetHandler.Setup(h => h.OrderType).Returns(targetOrderType);
targetHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => targetHandlerCalled = true)
.ReturnsAsync(RewardResult.Ok(targetRewardData));
// Create other handlers
var otherHandler1 = new Mock<IPaymentRewardHandler>();
otherHandler1.Setup(h => h.OrderType).Returns(otherOrderType1);
otherHandler1.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => otherHandler1Called = true)
.ReturnsAsync(RewardResult.Ok("other1_data"));
var otherHandler2 = new Mock<IPaymentRewardHandler>();
otherHandler2.Setup(h => h.OrderType).Returns(otherOrderType2);
otherHandler2.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => otherHandler2Called = true)
.ReturnsAsync(RewardResult.Ok("other2_data"));
var dispatcher = CreateDispatcher(new[]
{
otherHandler1.Object,
targetHandler.Object,
otherHandler2.Object
});
var paymentOrder = CreatePaymentOrder(seed.Get, targetOrderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Only target handler should be called, result should match target handler's output
return targetHandlerCalled &&
!otherHandler1Called &&
!otherHandler2Called &&
result.Success &&
result.RewardData == targetRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.7: GetHandler returns correct handler for registered order type
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetHandler_WithRegisteredOrderType_ShouldReturnCorrectHandler(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var handler = dispatcher.GetHandler(orderType);
// Assert: Should return the registered handler
return handler != null && handler.OrderType == orderType;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.8: GetHandler returns null for unregistered order type
/// If no handler is found, GetHandler should return null
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetHandler_WithUnregisteredOrderType_ShouldReturnNull(PositiveInt seed)
{
// Arrange
var registeredOrderType = GenerateValidOrderType(seed.Get);
var unregisteredOrderType = $"unregistered_{seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var handler = dispatcher.GetHandler(unregisteredOrderType);
// Assert: Should return null for unregistered order type
return handler == null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.9: HasHandler returns true for registered order type
/// The system should correctly identify if a handler exists for an order type
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool HasHandler_WithRegisteredOrderType_ShouldReturnTrue(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var hasHandler = dispatcher.HasHandler(orderType);
// Assert: Should return true for registered order type
return hasHandler;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.10: HasHandler returns false for unregistered order type
/// The system should correctly identify if no handler exists for an order type
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool HasHandler_WithUnregisteredOrderType_ShouldReturnFalse(PositiveInt seed)
{
// Arrange
var registeredOrderType = GenerateValidOrderType(seed.Get);
var unregisteredOrderType = $"unregistered_{seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var hasHandler = dispatcher.HasHandler(unregisteredOrderType);
// Assert: Should return false for unregistered order type
return !hasHandler;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.11: GetRegisteredOrderTypes returns all registered order types
/// The system should track all registered handlers
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetRegisteredOrderTypes_ShouldReturnAllRegisteredTypes(PositiveInt seed)
{
// Arrange
var orderType1 = $"type1_{seed.Get}";
var orderType2 = $"type2_{seed.Get}";
var orderType3 = $"type3_{seed.Get}";
var handler1 = new Mock<IPaymentRewardHandler>();
handler1.Setup(h => h.OrderType).Returns(orderType1);
var handler2 = new Mock<IPaymentRewardHandler>();
handler2.Setup(h => h.OrderType).Returns(orderType2);
var handler3 = new Mock<IPaymentRewardHandler>();
handler3.Setup(h => h.OrderType).Returns(orderType3);
var dispatcher = CreateDispatcher(new[]
{
handler1.Object,
handler2.Object,
handler3.Object
});
// Act
var registeredTypes = dispatcher.GetRegisteredOrderTypes();
// Assert: Should contain all registered order types
return registeredTypes.Count == 3 &&
registeredTypes.Contains(orderType1) &&
registeredTypes.Contains(orderType2) &&
registeredTypes.Contains(orderType3);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.12: ProcessReward with null order returns failure
/// The dispatcher should handle null orders gracefully
///
/// **Validates: Requirements 6.3**
/// </summary>
[Fact]
public void ProcessReward_WithNullOrder_ShouldReturnFailure()
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
// Act
var result = dispatcher.ProcessRewardAsync(null!).GetAwaiter().GetResult();
// Assert: Should return failure
Assert.False(result.Success);
Assert.NotNull(result.Message);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.13: ProcessReward with empty order type returns failure
/// The dispatcher should handle orders with empty order type gracefully
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithEmptyOrderType_ShouldReturnFailure(PositiveInt seed)
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
var paymentOrder = CreatePaymentOrder(seed.Get, "");
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return failure
return !result.Success && result.Message != null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.14: Order type matching is case-insensitive
/// The system should match order types regardless of case
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_OrderTypeMatching_ShouldBeCaseInsensitive(PositiveInt seed)
{
// Arrange
var lowerCaseOrderType = $"order_type_{seed.Get}".ToLowerInvariant();
var upperCaseOrderType = lowerCaseOrderType.ToUpperInvariant();
var expectedRewardData = $"{{\"reward\":{seed.Get}}}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(lowerCaseOrderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, upperCaseOrderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should find handler despite case difference
return result.Success && result.RewardData == expectedRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.15: Empty handlers collection returns success for any order
/// When no handlers are registered, processing should succeed with no reward
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithNoHandlers_ShouldReturnSuccess(PositiveInt seed)
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
var paymentOrder = CreatePaymentOrder(seed.Get, GenerateValidOrderType(seed.Get));
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return success with no reward data
return result.Success && result.RewardData == null;
}
#endregion
#region Helper Methods
/// <summary>
/// 创建 PaymentRewardDispatcher 实例
/// </summary>
private PaymentRewardDispatcher CreateDispatcher(IEnumerable<IPaymentRewardHandler> handlers)
{
return new PaymentRewardDispatcher(handlers, _mockLogger.Object);
}
/// <summary>
/// 创建测试用的 PaymentOrder
/// </summary>
private PaymentOrder CreatePaymentOrder(int seed, string orderType)
{
return new PaymentOrder
{
Id = seed,
OrderNo = $"PO{DateTime.Now:yyyyMMddHHmmss}{seed:D6}",
UserId = Math.Max(1, seed % 10000),
OrderType = orderType,
Title = $"测试订单 {seed}",
Amount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
PayAmount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
PayMethod = "wechat",
Status = 1, // 已支付
PaidAt = DateTime.Now,
TransactionId = $"TX_{seed}",
BizData = $"{{\"seed\":{seed}}}",
RewardStatus = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
}
/// <summary>
/// 生成有效的订单类型
/// </summary>
private string GenerateValidOrderType(int seed)
{
var types = new[] { "diamond_recharge", "vip_purchase", "gift_buy", "subscription", "premium" };
return $"{types[seed % types.Length]}_{seed % 1000}";
}
#endregion
}