using FsCheck; using FsCheck.Xunit; using NSubstitute; using Xunit; using XiangYi.Application.Services; using XiangYi.Core.Entities.Biz; using XiangYi.Core.Enums; using XiangYi.Core.Interfaces; using Microsoft.Extensions.Logging; namespace XiangYi.Application.Tests.Services; /// /// AdminOrderService属性测试 - 订单删除功能 /// public class AdminOrderDeletePropertyTests { /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// *For any* order, the order is deletable if and only if its status is NOT "已支付" (status = 2). /// Orders with status 1 (待支付), 3 (已取消), or 4 (已退款) SHALL be deletable. /// [Property(MaxTest = 100)] public Property CanDeleteOrder_PaidStatus_ShouldReturnFalse() { // Paid status (2) should NOT be deletable return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var paidStatus = (int)OrderStatus.Paid; // 2 var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(paidStatus); return !canDelete; }); } /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// Pending orders (status = 1) should be deletable /// [Property(MaxTest = 100)] public Property CanDeleteOrder_PendingStatus_ShouldReturnTrue() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var pendingStatus = (int)OrderStatus.Pending; // 1 var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(pendingStatus); return canDelete; }); } /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// Cancelled orders (status = 3) should be deletable /// [Property(MaxTest = 100)] public Property CanDeleteOrder_CancelledStatus_ShouldReturnTrue() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var cancelledStatus = (int)OrderStatus.Cancelled; // 3 var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(cancelledStatus); return canDelete; }); } /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// Refunded orders (status = 4) should be deletable /// [Property(MaxTest = 100)] public Property CanDeleteOrder_RefundedStatus_ShouldReturnTrue() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var refundedStatus = (int)OrderStatus.Refunded; // 4 var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(refundedStatus); return canDelete; }); } /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// *For any* valid order status, only paid status (2) should return false for CanDeleteOrder /// [Property(MaxTest = 100)] public Property CanDeleteOrder_AllValidStatuses_OnlyPaidShouldBeFalse() { var validStatusArb = Gen.Elements(1, 2, 3, 4).ToArbitrary(); return Prop.ForAll( validStatusArb, status => { var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(status); // Only status 2 (Paid) should return false var expectedCanDelete = status != (int)OrderStatus.Paid; return canDelete == expectedCanDelete; }); } /// /// **Feature: order-delete, Property 3: Order deletion eligibility is determined by status** /// **Validates: Requirements 2.1, 2.2, 2.3** /// /// *For any* random status value, the result should be consistent with the rule: /// deletable if and only if status != 2 /// [Property(MaxTest = 100)] public Property CanDeleteOrder_AnyStatus_ShouldFollowRule() { var statusArb = Gen.Choose(-10, 10).ToArbitrary(); return Prop.ForAll( statusArb, status => { var service = new AdminOrderService(null!, null!, null!, null!, null!); var canDelete = service.CanDeleteOrder(status); // Rule: deletable if and only if status != 2 (Paid) var expectedCanDelete = status != (int)OrderStatus.Paid; return canDelete == expectedCanDelete; }); } } /// /// AdminOrderService属性测试 - 软删除行为 /// public class AdminOrderSoftDeletePropertyTests { /// /// **Feature: order-delete, Property 1: Soft delete marks order as deleted** /// **Validates: Requirements 1.2** /// /// *For any* order that is eligible for deletion (status is pending, cancelled, or refunded), /// when the delete operation is performed, the order's IsDeleted field SHALL be set to true /// and DeleteTime SHALL be set to the current timestamp. /// [Property(MaxTest = 100)] public Property DeleteOrder_EligibleOrder_ShouldSetIsDeletedAndDeleteTime() { // Generate eligible statuses (1=Pending, 3=Cancelled, 4=Refunded) var eligibleStatusArb = Gen.Elements( (int)OrderStatus.Pending, (int)OrderStatus.Cancelled, (int)OrderStatus.Refunded ).ToArbitrary(); var orderIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary(); var adminIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary(); return Prop.ForAll( eligibleStatusArb, orderIdArb, adminIdArb, (status, orderId, adminId) => { // Arrange var order = new Order { Id = orderId, OrderNo = $"TEST{orderId}", Status = status, IsDeleted = false, DeleteTime = null }; var orderRepository = Substitute.For>(); orderRepository.GetByIdAsync(orderId).Returns(Task.FromResult(order)); orderRepository.UpdateAsync(Arg.Any()).Returns(Task.FromResult(1)); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act var beforeDelete = DateTime.Now; service.DeleteOrderAsync(orderId, adminId).Wait(); var afterDelete = DateTime.Now; // Assert return order.IsDeleted == true && order.DeleteTime != null && order.DeleteTime >= beforeDelete && order.DeleteTime <= afterDelete; }); } /// /// **Feature: order-delete, Property 1: Soft delete marks order as deleted** /// **Validates: Requirements 1.2** /// /// Soft delete should preserve the original order data (only IsDeleted and DeleteTime change) /// [Property(MaxTest = 100)] public Property DeleteOrder_ShouldPreserveOriginalData() { var eligibleStatusArb = Gen.Elements( (int)OrderStatus.Pending, (int)OrderStatus.Cancelled, (int)OrderStatus.Refunded ).ToArbitrary(); var orderIdArb = Gen.Choose(1, int.MaxValue).Select(x => (long)x).ToArbitrary(); var amountArb = Gen.Choose(1, 10000).Select(x => (decimal)x).ToArbitrary(); return Prop.ForAll( eligibleStatusArb, orderIdArb, amountArb, (status, orderId, amount) => { // Arrange var userId = orderId + 1000; // Derive userId from orderId var originalOrderNo = $"TEST{orderId}"; var originalProductName = "Test Product"; var originalOrderType = 1; var order = new Order { Id = orderId, OrderNo = originalOrderNo, UserId = userId, Status = status, Amount = amount, PayAmount = amount, ProductName = originalProductName, OrderType = originalOrderType, IsDeleted = false, DeleteTime = null }; var orderRepository = Substitute.For>(); orderRepository.GetByIdAsync(orderId).Returns(Task.FromResult(order)); orderRepository.UpdateAsync(Arg.Any()).Returns(Task.FromResult(1)); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act service.DeleteOrderAsync(orderId, 1).Wait(); // Assert - Original data should be preserved return order.OrderNo == originalOrderNo && order.UserId == userId && order.Status == status && order.Amount == amount && order.ProductName == originalProductName && order.OrderType == originalOrderType && order.IsDeleted == true; // Only IsDeleted should change }); } } /// /// AdminOrderService属性测试 - 批量删除过滤 /// public class AdminOrderBatchDeletePropertyTests { /// /// **Feature: order-delete, Property 4: Batch deletion correctly filters and reports results** /// **Validates: Requirements 3.3, 3.4** /// /// *For any* batch of order IDs submitted for deletion, the operation SHALL delete only /// eligible orders (non-paid status), skip ineligible orders, and return accurate counts /// of successful deletions and skipped orders. /// [Property(MaxTest = 100)] public Property BatchDelete_ShouldCorrectlyFilterAndReportResults() { // Generate a mix of order statuses var orderCountArb = Gen.Choose(1, 10).ToArbitrary(); return Prop.ForAll( orderCountArb, orderCount => { // Arrange - Create orders with mixed statuses var orders = new List(); var orderIds = new List(); var expectedSuccessCount = 0; var expectedSkippedCount = 0; var expectedSkippedIds = new List(); for (int i = 1; i <= orderCount; i++) { var orderId = (long)i; // Alternate between statuses: 1, 2, 3, 4, 1, 2, 3, 4... var status = ((i - 1) % 4) + 1; var order = new Order { Id = orderId, OrderNo = $"TEST{orderId}", Status = status, IsDeleted = false, DeleteTime = null }; orders.Add(order); orderIds.Add(orderId); if (status == (int)OrderStatus.Paid) { expectedSkippedCount++; expectedSkippedIds.Add(orderId); } else { expectedSuccessCount++; } } var orderRepository = Substitute.For>(); orderRepository.GetListAsync(Arg.Any>>()) .Returns(Task.FromResult>(orders)); orderRepository.UpdateAsync(Arg.Any()).Returns(Task.FromResult(1)); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act var result = service.BatchDeleteOrdersAsync(orderIds, 1).Result; // Assert return result.SuccessCount == expectedSuccessCount && result.SkippedCount == expectedSkippedCount && result.SkippedOrderIds.Count == expectedSkippedIds.Count; }); } /// /// **Feature: order-delete, Property 4: Batch deletion correctly filters and reports results** /// **Validates: Requirements 3.3, 3.4** /// /// When all orders are eligible (non-paid), all should be deleted successfully /// [Property(MaxTest = 100)] public Property BatchDelete_AllEligible_ShouldDeleteAll() { var orderCountArb = Gen.Choose(1, 10).ToArbitrary(); var eligibleStatusArb = Gen.Elements( (int)OrderStatus.Pending, (int)OrderStatus.Cancelled, (int)OrderStatus.Refunded ).ToArbitrary(); return Prop.ForAll( orderCountArb, eligibleStatusArb, (orderCount, status) => { // Arrange - Create orders with only eligible statuses var orders = new List(); var orderIds = new List(); for (int i = 1; i <= orderCount; i++) { var orderId = (long)i; var order = new Order { Id = orderId, OrderNo = $"TEST{orderId}", Status = status, IsDeleted = false, DeleteTime = null }; orders.Add(order); orderIds.Add(orderId); } var orderRepository = Substitute.For>(); orderRepository.GetListAsync(Arg.Any>>()) .Returns(Task.FromResult>(orders)); orderRepository.UpdateAsync(Arg.Any()).Returns(Task.FromResult(1)); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act var result = service.BatchDeleteOrdersAsync(orderIds, 1).Result; // Assert return result.SuccessCount == orderCount && result.SkippedCount == 0 && result.SkippedOrderIds.Count == 0; }); } /// /// **Feature: order-delete, Property 4: Batch deletion correctly filters and reports results** /// **Validates: Requirements 3.3, 3.4** /// /// When all orders are paid (ineligible), none should be deleted /// [Property(MaxTest = 100)] public Property BatchDelete_AllPaid_ShouldSkipAll() { var orderCountArb = Gen.Choose(1, 10).ToArbitrary(); return Prop.ForAll( orderCountArb, orderCount => { // Arrange - Create orders with only paid status var orders = new List(); var orderIds = new List(); for (int i = 1; i <= orderCount; i++) { var orderId = (long)i; var order = new Order { Id = orderId, OrderNo = $"TEST{orderId}", Status = (int)OrderStatus.Paid, IsDeleted = false, DeleteTime = null }; orders.Add(order); orderIds.Add(orderId); } var orderRepository = Substitute.For>(); orderRepository.GetListAsync(Arg.Any>>()) .Returns(Task.FromResult>(orders)); orderRepository.UpdateAsync(Arg.Any()).Returns(Task.FromResult(1)); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act var result = service.BatchDeleteOrdersAsync(orderIds, 1).Result; // Assert return result.SuccessCount == 0 && result.SkippedCount == orderCount && result.SkippedOrderIds.Count == orderCount; }); } /// /// **Feature: order-delete, Property 4: Batch deletion correctly filters and reports results** /// **Validates: Requirements 3.3, 3.4** /// /// Empty order list should return zero counts /// [Property(MaxTest = 100)] public Property BatchDelete_EmptyList_ShouldReturnZeroCounts() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { // Arrange var orderIds = new List(); var orderRepository = Substitute.For>(); var userRepository = Substitute.For>(); var memberRepository = Substitute.For>(); var weChatService = Substitute.For(); var logger = Substitute.For>(); var service = new AdminOrderService( orderRepository, userRepository, memberRepository, weChatService, logger); // Act var result = service.BatchDeleteOrdersAsync(orderIds, 1).Result; // Assert return result.SuccessCount == 0 && result.SkippedCount == 0 && result.SkippedOrderIds.Count == 0; }); } }