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; /// /// 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** /// public class PaymentRewardDispatcherPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 5: Reward Handler Dispatch /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(orderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .Callback(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; } /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(differentOrderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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; } /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(orderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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; } /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(orderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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; } /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(orderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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); } /// /// 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** /// [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(); targetHandler.Setup(h => h.OrderType).Returns(targetOrderType); targetHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .Callback(() => targetHandlerCalled = true) .ReturnsAsync(RewardResult.Ok(targetRewardData)); // Create other handlers var otherHandler1 = new Mock(); otherHandler1.Setup(h => h.OrderType).Returns(otherOrderType1); otherHandler1.Setup(h => h.ProcessRewardAsync(It.IsAny())) .Callback(() => otherHandler1Called = true) .ReturnsAsync(RewardResult.Ok("other1_data")); var otherHandler2 = new Mock(); otherHandler2.Setup(h => h.OrderType).Returns(otherOrderType2); otherHandler2.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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; } /// /// 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** /// [Property(MaxTest = 100)] public bool GetHandler_WithRegisteredOrderType_ShouldReturnCorrectHandler(PositiveInt seed) { // Arrange var orderType = GenerateValidOrderType(seed.Get); var mockHandler = new Mock(); 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; } /// /// 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** /// [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(); 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; } /// /// 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** /// [Property(MaxTest = 100)] public bool HasHandler_WithRegisteredOrderType_ShouldReturnTrue(PositiveInt seed) { // Arrange var orderType = GenerateValidOrderType(seed.Get); var mockHandler = new Mock(); 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; } /// /// 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** /// [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(); 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; } /// /// 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** /// [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(); handler1.Setup(h => h.OrderType).Returns(orderType1); var handler2 = new Mock(); handler2.Setup(h => h.OrderType).Returns(orderType2); var handler3 = new Mock(); 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); } /// /// 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** /// [Fact] public void ProcessReward_WithNullOrder_ShouldReturnFailure() { // Arrange var dispatcher = CreateDispatcher(Array.Empty()); // Act var result = dispatcher.ProcessRewardAsync(null!).GetAwaiter().GetResult(); // Assert: Should return failure Assert.False(result.Success); Assert.NotNull(result.Message); } /// /// 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** /// [Property(MaxTest = 100)] public bool ProcessReward_WithEmptyOrderType_ShouldReturnFailure(PositiveInt seed) { // Arrange var dispatcher = CreateDispatcher(Array.Empty()); var paymentOrder = CreatePaymentOrder(seed.Get, ""); // Act var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult(); // Assert: Should return failure return !result.Success && result.Message != null; } /// /// 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** /// [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(); mockHandler.Setup(h => h.OrderType).Returns(lowerCaseOrderType); mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny())) .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; } /// /// 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** /// [Property(MaxTest = 100)] public bool ProcessReward_WithNoHandlers_ShouldReturnSuccess(PositiveInt seed) { // Arrange var dispatcher = CreateDispatcher(Array.Empty()); 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 /// /// 创建 PaymentRewardDispatcher 实例 /// private PaymentRewardDispatcher CreateDispatcher(IEnumerable handlers) { return new PaymentRewardDispatcher(handlers, _mockLogger.Object); } /// /// 创建测试用的 PaymentOrder /// 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 }; } /// /// 生成有效的订单类型 /// private string GenerateValidOrderType(int seed) { var types = new[] { "diamond_recharge", "vip_purchase", "gift_buy", "subscription", "premium" }; return $"{types[seed % types.Length]}_{seed % 1000}"; } #endregion }