mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/OrderPropertyTests.cs
zpc 6bf2ea595c feat(admin-business): 完成后台管理系统全部业务模块
- 系统配置管理模块 (Config)
- 内容管理模块 (Banner, Promotion)
- 测评管理模块 (Type, Question, Category, Mapping, Conclusion)
- 用户管理模块 (User)
- 订单管理模块 (Order)
- 规划师管理模块 (Planner)
- 分销管理模块 (InviteCode, Commission, Withdrawal)
- 数据统计仪表盘模块 (Dashboard)
- 权限控制集成
- 服务注册配置

全部381个测试通过
2026-02-03 20:50:51 +08:00

411 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// Order 属性测试
/// 验证订单服务的正确性属性
/// </summary>
public class OrderPropertyTests
{
private readonly Mock<ILogger<OrderService>> _mockLogger = new();
#region Property 12: Refund Status Transitions
/// <summary>
/// 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**
/// </summary>
[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;
}
/// <summary>
/// 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**
/// </summary>
[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;
}
/// <summary>
/// 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**
/// </summary>
[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;
}
/// <summary>
/// Property 12: 退款时记录RefundAmount
/// *For any* refund operation, the RefundAmount SHALL be recorded.
///
/// **Validates: Requirements 11.3**
/// </summary>
[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;
}
/// <summary>
/// Property 12: 非已支付/已完成状态的订单不能发起退款
/// *For any* order with Status not in [2, 3], refund operation SHALL fail.
///
/// **Validates: Requirements 11.1**
/// </summary>
[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;
}
}
/// <summary>
/// Property 12: 退款金额不能超过实付金额
/// *For any* refund operation, the RefundAmount SHALL NOT exceed PayAmount.
///
/// **Validates: Requirements 11.3**
/// </summary>
[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;
}
}
/// <summary>
/// Property 12: 退款金额必须大于0
/// *For any* refund operation, the RefundAmount SHALL be greater than 0.
///
/// **Validates: Requirements 11.3**
/// </summary>
[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;
}
}
/// <summary>
/// Property 12: 退款失败时状态回滚
/// *For any* refund failure, the order Status SHALL revert and error reason SHALL be recorded.
///
/// **Validates: Requirements 11.4**
/// </summary>
[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;
}
/// <summary>
/// Property 12: 只有退款中状态的订单可以完成退款
/// *For any* order with Status not equal to 4, complete refund operation SHALL fail.
///
/// **Validates: Requirements 11.3**
/// </summary>
[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;
}
}
/// <summary>
/// Property 12: CanRefund 正确判断订单是否可退款
/// *For any* order, CanRefund SHALL return true only when Status is 2 or 3.
///
/// **Validates: Requirements 11.1**
/// </summary>
[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;
}
/// <summary>
/// Property 12: 退款操作记录退款原因
/// *For any* refund operation with a reason, the RefundReason SHALL be recorded.
///
/// **Validates: Requirements 11.6**
/// </summary>
[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
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
/// <summary>
/// 创建测试订单
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="seed">随机种子</param>
/// <param name="status">订单状态</param>
/// <param name="payAmount">实付金额</param>
/// <returns>订单ID</returns>
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;
}
/// <summary>
/// 创建测试订单指定用户ID
/// </summary>
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
}