xiangyixiangqin/server/tests/XiangYi.Application.Tests/Services/AdminOrderServicePropertyTests.cs
2026-01-06 20:56:09 +08:00

526 lines
20 KiB
C#

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;
/// <summary>
/// AdminOrderService属性测试 - 订单删除功能
/// </summary>
public class AdminOrderDeletePropertyTests
{
/// <summary>
/// **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.
/// </summary>
[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;
});
}
/// <summary>
/// **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
/// </summary>
[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;
});
}
/// <summary>
/// **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
/// </summary>
[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;
});
}
/// <summary>
/// **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
/// </summary>
[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;
});
}
/// <summary>
/// **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
/// </summary>
[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;
});
}
/// <summary>
/// **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
/// </summary>
[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;
});
}
}
/// <summary>
/// AdminOrderService属性测试 - 软删除行为
/// </summary>
public class AdminOrderSoftDeletePropertyTests
{
/// <summary>
/// **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.
/// </summary>
[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<IRepository<Order>>();
orderRepository.GetByIdAsync(orderId).Returns(Task.FromResult<Order?>(order));
orderRepository.UpdateAsync(Arg.Any<Order>()).Returns(Task.FromResult(1));
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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;
});
}
/// <summary>
/// **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)
/// </summary>
[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<IRepository<Order>>();
orderRepository.GetByIdAsync(orderId).Returns(Task.FromResult<Order?>(order));
orderRepository.UpdateAsync(Arg.Any<Order>()).Returns(Task.FromResult(1));
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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
});
}
}
/// <summary>
/// AdminOrderService属性测试 - 批量删除过滤
/// </summary>
public class AdminOrderBatchDeletePropertyTests
{
/// <summary>
/// **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.
/// </summary>
[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<Order>();
var orderIds = new List<long>();
var expectedSuccessCount = 0;
var expectedSkippedCount = 0;
var expectedSkippedIds = new List<long>();
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<IRepository<Order>>();
orderRepository.GetListAsync(Arg.Any<System.Linq.Expressions.Expression<Func<Order, bool>>>())
.Returns(Task.FromResult<List<Order>>(orders));
orderRepository.UpdateAsync(Arg.Any<Order>()).Returns(Task.FromResult(1));
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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;
});
}
/// <summary>
/// **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
/// </summary>
[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<Order>();
var orderIds = new List<long>();
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<IRepository<Order>>();
orderRepository.GetListAsync(Arg.Any<System.Linq.Expressions.Expression<Func<Order, bool>>>())
.Returns(Task.FromResult<List<Order>>(orders));
orderRepository.UpdateAsync(Arg.Any<Order>()).Returns(Task.FromResult(1));
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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;
});
}
/// <summary>
/// **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
/// </summary>
[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<Order>();
var orderIds = new List<long>();
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<IRepository<Order>>();
orderRepository.GetListAsync(Arg.Any<System.Linq.Expressions.Expression<Func<Order, bool>>>())
.Returns(Task.FromResult<List<Order>>(orders));
orderRepository.UpdateAsync(Arg.Any<Order>()).Returns(Task.FromResult(1));
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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;
});
}
/// <summary>
/// **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
/// </summary>
[Property(MaxTest = 100)]
public Property BatchDelete_EmptyList_ShouldReturnZeroCounts()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
_ =>
{
// Arrange
var orderIds = new List<long>();
var orderRepository = Substitute.For<IRepository<Order>>();
var userRepository = Substitute.For<IRepository<User>>();
var memberRepository = Substitute.For<IRepository<Member>>();
var weChatService = Substitute.For<Infrastructure.WeChat.IWeChatService>();
var logger = Substitute.For<ILogger<AdminOrderService>>();
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;
});
}
}