using FsCheck; using FsCheck.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MiAssessment.Admin.Business.Data; using MiAssessment.Admin.Business.Entities; using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Services; using Moq; using Xunit; namespace MiAssessment.Tests.Admin; /// /// Order 属性测试 /// 验证订单服务的正确性属性 /// public class OrderPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 12: Refund Status Transitions /// /// Property 12: 退款操作将订单状态从已支付(2)转换为退款中(4) /// *For any* refund operation on a paid order, the order Status SHALL transition /// from 2 (paid) to 4 (refunding). /// /// **Validates: Requirements 11.1, 11.2** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_PaidToRefunding(PositiveInt seed) { // Arrange: 创建已支付状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act: 发起退款 var result = service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult(); // Assert: 订单状态应该变为退款中(4) var order = dbContext.Orders.Find(orderId); return result && order != null && order.Status == 4; } /// /// Property 12: 退款操作将订单状态从已完成(3)转换为退款中(4) /// *For any* refund operation on a completed order, the order Status SHALL transition /// from 3 (completed) to 4 (refunding). /// /// **Validates: Requirements 11.1, 11.2** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_CompletedToRefunding(PositiveInt seed) { // Arrange: 创建已完成状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 3, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act: 发起退款 var result = service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult(); // Assert: 订单状态应该变为退款中(4) var order = dbContext.Orders.Find(orderId); return result && order != null && order.Status == 4; } /// /// Property 12: 完成退款将订单状态从退款中(4)转换为已退款(5),并记录RefundTime /// *For any* refund completion, the order Status SHALL transition from 4 (refunding) /// to 5 (refunded) with RefundTime recorded. /// /// **Validates: Requirements 11.3** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_RefundingToRefunded(PositiveInt seed) { // Arrange: 创建退款中状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 4, payAmount: 100m); // 设置退款金额(模拟已发起退款) var order = dbContext.Orders.Find(orderId)!; order.RefundAmount = 50m; dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object); // Act: 完成退款 var result = service.CompleteRefundAsync(orderId).GetAwaiter().GetResult(); // Assert: 订单状态应该变为已退款(5),且RefundTime已记录 dbContext.Entry(order).Reload(); return result && order.Status == 5 && order.RefundTime.HasValue; } /// /// Property 12: 退款时记录RefundAmount /// *For any* refund operation, the RefundAmount SHALL be recorded. /// /// **Validates: Requirements 11.3** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_RefundAmountRecorded(PositiveInt seed) { // Arrange: 创建已支付状态的订单 using var dbContext = CreateDbContext(); var payAmount = (seed.Get % 1000) + 100m; // 100-1099 var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: payAmount); var service = new OrderService(dbContext, _mockLogger.Object); // 计算有效的退款金额(不超过实付金额) var refundAmount = Math.Min((seed.Get % 100) + 1m, payAmount); // Act: 发起退款 var result = service.RefundAsync(orderId, refundAmount, "测试退款").GetAwaiter().GetResult(); // Assert: RefundAmount 应该被正确记录 var order = dbContext.Orders.Find(orderId); return result && order != null && order.RefundAmount == refundAmount; } /// /// Property 12: 非已支付/已完成状态的订单不能发起退款 /// *For any* order with Status not in [2, 3], refund operation SHALL fail. /// /// **Validates: Requirements 11.1** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_InvalidStatusCannotRefund(PositiveInt seed) { // Arrange: 创建非已支付/已完成状态的订单 using var dbContext = CreateDbContext(); // 状态: 1待支付, 4退款中, 5已退款, 6已取消 var invalidStatuses = new[] { 1, 4, 5, 6 }; var status = invalidStatuses[seed.Get % invalidStatuses.Length]; var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act & Assert: 发起退款应该抛出异常 try { service.RefundAsync(orderId, 50m, "测试退款").GetAwaiter().GetResult(); return false; // 不应该成功 } catch (BusinessException ex) { return ex.Code == ErrorCodes.OrderCannotRefund; } } /// /// Property 12: 退款金额不能超过实付金额 /// *For any* refund operation, the RefundAmount SHALL NOT exceed PayAmount. /// /// **Validates: Requirements 11.3** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_RefundAmountNotExceedPayAmount(PositiveInt seed) { // Arrange: 创建已支付状态的订单 using var dbContext = CreateDbContext(); var payAmount = (seed.Get % 100) + 10m; // 10-109 var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: payAmount); var service = new OrderService(dbContext, _mockLogger.Object); // 退款金额超过实付金额 var refundAmount = payAmount + 1m; // Act & Assert: 发起退款应该抛出异常 try { service.RefundAsync(orderId, refundAmount, "测试退款").GetAwaiter().GetResult(); return false; // 不应该成功 } catch (BusinessException ex) { return ex.Code == ErrorCodes.RefundAmountInvalid; } } /// /// Property 12: 退款金额必须大于0 /// *For any* refund operation, the RefundAmount SHALL be greater than 0. /// /// **Validates: Requirements 11.3** /// [Property(MaxTest = 50)] public bool RefundStatusTransition_RefundAmountMustBePositive(PositiveInt seed) { // Arrange: 创建已支付状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act & Assert: 退款金额为0或负数应该抛出异常 try { service.RefundAsync(orderId, 0m, "测试退款").GetAwaiter().GetResult(); return false; // 不应该成功 } catch (BusinessException ex) { return ex.Code == ErrorCodes.RefundAmountInvalid; } } /// /// Property 12: 退款失败时状态回滚 /// *For any* refund failure, the order Status SHALL revert and error reason SHALL be recorded. /// /// **Validates: Requirements 11.4** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_FailedRefundRollback(PositiveInt seed) { // Arrange: 创建退款中状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 4, payAmount: 100m); var order = dbContext.Orders.Find(orderId)!; order.RefundAmount = 50m; dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object); var errorReason = $"退款失败原因_{seed.Get}"; // Act: 退款失败回滚 var result = service.RefundFailedAsync(orderId, errorReason).GetAwaiter().GetResult(); // Assert: 订单状态应该回滚到已支付(2),且记录失败原因 dbContext.Entry(order).Reload(); return result && order.Status == 2 && order.RefundReason == errorReason; } /// /// Property 12: 只有退款中状态的订单可以完成退款 /// *For any* order with Status not equal to 4, complete refund operation SHALL fail. /// /// **Validates: Requirements 11.3** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_OnlyRefundingCanComplete(PositiveInt seed) { // Arrange: 创建非退款中状态的订单 using var dbContext = CreateDbContext(); var invalidStatuses = new[] { 1, 2, 3, 5, 6 }; var status = invalidStatuses[seed.Get % invalidStatuses.Length]; var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act & Assert: 完成退款应该抛出异常 try { service.CompleteRefundAsync(orderId).GetAwaiter().GetResult(); return false; // 不应该成功 } catch (BusinessException ex) { return ex.Code == ErrorCodes.OrderCannotRefund; } } /// /// Property 12: CanRefund 正确判断订单是否可退款 /// *For any* order, CanRefund SHALL return true only when Status is 2 or 3. /// /// **Validates: Requirements 11.1** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_CanRefundCorrectness(PositiveInt seed) { // Arrange: 创建不同状态的订单 using var dbContext = CreateDbContext(); var allStatuses = new[] { 1, 2, 3, 4, 5, 6 }; var status = allStatuses[seed.Get % allStatuses.Length]; var orderId = CreateTestOrder(dbContext, seed.Get, status: status, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); // Act var canRefund = service.CanRefundAsync(orderId).GetAwaiter().GetResult(); // Assert: 只有状态2或3可以退款 var expectedCanRefund = status == 2 || status == 3; return canRefund == expectedCanRefund; } /// /// Property 12: 退款操作记录退款原因 /// *For any* refund operation with a reason, the RefundReason SHALL be recorded. /// /// **Validates: Requirements 11.6** /// [Property(MaxTest = 100)] public bool RefundStatusTransition_RefundReasonRecorded(PositiveInt seed) { // Arrange: 创建已支付状态的订单 using var dbContext = CreateDbContext(); var orderId = CreateTestOrder(dbContext, seed.Get, status: 2, payAmount: 100m); var service = new OrderService(dbContext, _mockLogger.Object); var refundReason = $"退款原因_{seed.Get}"; // Act: 发起退款 var result = service.RefundAsync(orderId, 50m, refundReason).GetAwaiter().GetResult(); // Assert: RefundReason 应该被正确记录 var order = dbContext.Orders.Find(orderId); return result && order != null && order.RefundReason == refundReason; } #endregion #region 辅助方法 /// /// 创建内存数据库上下文 /// private AdminBusinessDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new AdminBusinessDbContext(options); } /// /// 创建测试订单 /// /// 数据库上下文 /// 随机种子 /// 订单状态 /// 实付金额 /// 订单ID private long CreateTestOrder(AdminBusinessDbContext dbContext, int seed, int status, decimal payAmount) { // 先创建用户 var user = new User { Uid = $"{seed % 1000000:D6}", OpenId = $"openid_{seed}", Phone = $"138{seed % 100000000:D8}", Nickname = $"User_{seed}", UserLevel = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Users.Add(user); dbContext.SaveChanges(); // 创建订单 var order = new Order { OrderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{seed % 10000:D4}", UserId = user.Id, OrderType = (seed % 2) + 1, // 1或2 ProductId = seed % 100 + 1, ProductName = $"测试商品_{seed}", Amount = payAmount, PayAmount = payAmount, PayType = 1, // 微信支付 Status = status, PayTime = status >= 2 ? DateTime.Now.AddMinutes(-seed % 60) : null, CreateTime = DateTime.Now.AddDays(-seed % 30), UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Orders.Add(order); dbContext.SaveChanges(); return order.Id; } /// /// 创建测试订单(指定用户ID) /// private long CreateTestOrderWithUser(AdminBusinessDbContext dbContext, int seed, int status, decimal payAmount, long userId) { var order = new Order { OrderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{seed % 10000:D4}", UserId = userId, OrderType = (seed % 2) + 1, ProductId = seed % 100 + 1, ProductName = $"测试商品_{seed}", Amount = payAmount, PayAmount = payAmount, PayType = 1, Status = status, PayTime = status >= 2 ? DateTime.Now.AddMinutes(-seed % 60) : null, CreateTime = DateTime.Now.AddDays(-seed % 30), UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Orders.Add(order); dbContext.SaveChanges(); return order.Id; } #endregion }