mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/WithdrawalPropertyTests.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

553 lines
20 KiB
C#

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.Models.Distribution;
using MiAssessment.Admin.Business.Services;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Admin;
/// <summary>
/// Withdrawal 属性测试
/// 验证提现审核服务的正确性属性
/// </summary>
public class WithdrawalPropertyTests
{
private readonly Mock<ILogger<DistributionService>> _mockLogger = new();
#region Property 15: Withdrawal Status Transitions
/// <summary>
/// Property 15: 审批通过将提现状态从待审核(1)转换为处理中(2)
/// *For any* withdrawal approval, the Status SHALL transition from 1 (pending)
/// to 2 (processing).
///
/// **Validates: Requirements 16.4**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_PendingToProcessing(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new ApproveWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}"
};
// Act: 审批通过
var result = service.ApproveWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: 提现状态应该变为处理中(2)
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.Status == 2;
}
/// <summary>
/// Property 15: 审批通过时记录AuditUserId
/// *For any* withdrawal approval, the AuditUserId SHALL be recorded.
///
/// **Validates: Requirements 16.4**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_ApproveRecordsAuditUserId(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var auditUserId = (long)(seed.Get % 1000 + 1);
var request = new ApproveWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = auditUserId,
AuditUserName = $"审核人_{seed.Get}"
};
// Act: 审批通过
var result = service.ApproveWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: AuditUserId 应该被正确记录
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.AuditUserId == auditUserId;
}
/// <summary>
/// Property 15: 审批通过时记录AuditTime
/// *For any* withdrawal approval, the AuditTime SHALL be recorded.
///
/// **Validates: Requirements 16.4**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_ApproveRecordsAuditTime(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var beforeApprove = DateTime.Now;
var request = new ApproveWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}"
};
// Act: 审批通过
var result = service.ApproveWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: AuditTime 应该被记录且在合理范围内
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result
&& withdrawal != null
&& withdrawal.AuditTime.HasValue
&& withdrawal.AuditTime.Value >= beforeApprove;
}
/// <summary>
/// Property 15: 拒绝将提现状态从待审核(1)转换为已拒绝(4)
/// *For any* withdrawal rejection, the Status SHALL transition from 1 (pending)
/// to 4 (cancelled).
///
/// **Validates: Requirements 16.5**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_PendingToCancelled(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new RejectWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}",
AuditRemark = $"拒绝原因_{seed.Get}"
};
// Act: 拒绝提现
var result = service.RejectWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: 提现状态应该变为已拒绝(4)
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.Status == 4;
}
/// <summary>
/// Property 15: 拒绝时记录AuditRemark
/// *For any* withdrawal rejection, the AuditRemark SHALL be recorded.
///
/// **Validates: Requirements 16.5**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_RejectRecordsAuditRemark(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var auditRemark = $"拒绝原因_{seed.Get}";
var request = new RejectWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}",
AuditRemark = auditRemark
};
// Act: 拒绝提现
var result = service.RejectWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: AuditRemark 应该被正确记录
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.AuditRemark == auditRemark;
}
/// <summary>
/// Property 15: 拒绝时回滚用户余额
/// *For any* withdrawal rejection, the user's Balance SHALL be restored.
///
/// **Validates: Requirements 16.7**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_RejectRestoresBalance(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, userId) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
// 获取提现金额和用户当前余额
var withdrawal = dbContext.Withdrawals.Find(withdrawalId)!;
var withdrawalAmount = withdrawal.Amount;
var user = dbContext.Users.Find(userId)!;
var balanceBeforeReject = user.Balance;
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new RejectWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}",
AuditRemark = $"拒绝原因_{seed.Get}"
};
// Act: 拒绝提现
var result = service.RejectWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: 用户余额应该恢复
dbContext.Entry(user).Reload();
return result && user.Balance == balanceBeforeReject + withdrawalAmount;
}
/// <summary>
/// Property 15: 完成提现将状态从处理中(2)转换为已完成(3)
/// *For any* withdrawal completion, the Status SHALL transition from 2 (processing)
/// to 3 (completed).
///
/// **Validates: Requirements 16.6**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_ProcessingToCompleted(PositiveInt seed)
{
// Arrange: 创建处理中状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 2);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new CompleteWithdrawalRequest
{
Id = withdrawalId,
PayTransactionId = $"TXN{DateTime.Now:yyyyMMddHHmmss}{seed.Get % 10000:D4}"
};
// Act: 完成提现
var result = service.CompleteWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: 提现状态应该变为已完成(3)
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.Status == 3;
}
/// <summary>
/// Property 15: 完成提现时记录PayTime
/// *For any* withdrawal completion, the PayTime SHALL be recorded.
///
/// **Validates: Requirements 16.6**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_CompleteRecordsPayTime(PositiveInt seed)
{
// Arrange: 创建处理中状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 2);
var service = new DistributionService(dbContext, _mockLogger.Object);
var beforeComplete = DateTime.Now;
var request = new CompleteWithdrawalRequest
{
Id = withdrawalId,
PayTransactionId = $"TXN{DateTime.Now:yyyyMMddHHmmss}{seed.Get % 10000:D4}"
};
// Act: 完成提现
var result = service.CompleteWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: PayTime 应该被记录且在合理范围内
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result
&& withdrawal != null
&& withdrawal.PayTime.HasValue
&& withdrawal.PayTime.Value >= beforeComplete;
}
/// <summary>
/// Property 15: 完成提现时记录PayTransactionId
/// *For any* withdrawal completion, the PayTransactionId SHALL be recorded.
///
/// **Validates: Requirements 16.6**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_CompleteRecordsPayTransactionId(PositiveInt seed)
{
// Arrange: 创建处理中状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 2);
var service = new DistributionService(dbContext, _mockLogger.Object);
var payTransactionId = $"TXN{DateTime.Now:yyyyMMddHHmmss}{seed.Get % 10000:D4}";
var request = new CompleteWithdrawalRequest
{
Id = withdrawalId,
PayTransactionId = payTransactionId
};
// Act: 完成提现
var result = service.CompleteWithdrawalAsync(request).GetAwaiter().GetResult();
// Assert: PayTransactionId 应该被正确记录
var withdrawal = dbContext.Withdrawals.Find(withdrawalId);
return result && withdrawal != null && withdrawal.PayTransactionId == payTransactionId;
}
/// <summary>
/// Property 15: 非待审核状态的提现不能审批通过
/// *For any* withdrawal with Status not equal to 1, approve operation SHALL fail.
///
/// **Validates: Requirements 16.4**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_OnlyPendingCanApprove(PositiveInt seed)
{
// Arrange: 创建非待审核状态的提现记录
using var dbContext = CreateDbContext();
var invalidStatuses = new[] { 2, 3, 4 };
var status = invalidStatuses[seed.Get % invalidStatuses.Length];
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: status);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new ApproveWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}"
};
// Act & Assert: 审批通过应该抛出异常
try
{
service.ApproveWithdrawalAsync(request).GetAwaiter().GetResult();
return false; // 不应该成功
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.WithdrawalCannotApprove;
}
}
/// <summary>
/// Property 15: 非待审核状态的提现不能拒绝
/// *For any* withdrawal with Status not equal to 1, reject operation SHALL fail.
///
/// **Validates: Requirements 16.5**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_OnlyPendingCanReject(PositiveInt seed)
{
// Arrange: 创建非待审核状态的提现记录
using var dbContext = CreateDbContext();
var invalidStatuses = new[] { 2, 3, 4 };
var status = invalidStatuses[seed.Get % invalidStatuses.Length];
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: status);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new RejectWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}",
AuditRemark = $"拒绝原因_{seed.Get}"
};
// Act & Assert: 拒绝应该抛出异常
try
{
service.RejectWithdrawalAsync(request).GetAwaiter().GetResult();
return false; // 不应该成功
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.WithdrawalCannotReject;
}
}
/// <summary>
/// Property 15: 非处理中状态的提现不能完成
/// *For any* withdrawal with Status not equal to 2, complete operation SHALL fail.
///
/// **Validates: Requirements 16.6**
/// </summary>
[Property(MaxTest = 100)]
public bool WithdrawalStatusTransition_OnlyProcessingCanComplete(PositiveInt seed)
{
// Arrange: 创建非处理中状态的提现记录
using var dbContext = CreateDbContext();
var invalidStatuses = new[] { 1, 3, 4 };
var status = invalidStatuses[seed.Get % invalidStatuses.Length];
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: status);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new CompleteWithdrawalRequest
{
Id = withdrawalId,
PayTransactionId = $"TXN{DateTime.Now:yyyyMMddHHmmss}{seed.Get % 10000:D4}"
};
// Act & Assert: 完成应该抛出异常
try
{
service.CompleteWithdrawalAsync(request).GetAwaiter().GetResult();
return false; // 不应该成功
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.WithdrawalCannotComplete;
}
}
/// <summary>
/// Property 15: 拒绝提现时AuditRemark不能为空
/// *For any* withdrawal rejection, the AuditRemark SHALL NOT be empty.
///
/// **Validates: Requirements 16.5**
/// </summary>
[Property(MaxTest = 50)]
public bool WithdrawalStatusTransition_RejectRequiresAuditRemark(PositiveInt seed)
{
// Arrange: 创建待审核状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 1);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new RejectWithdrawalRequest
{
Id = withdrawalId,
AuditUserId = seed.Get % 1000 + 1,
AuditUserName = $"审核人_{seed.Get}",
AuditRemark = "" // 空的拒绝原因
};
// Act & Assert: 拒绝应该抛出异常
try
{
service.RejectWithdrawalAsync(request).GetAwaiter().GetResult();
return false; // 不应该成功
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ParamError;
}
}
/// <summary>
/// Property 15: 完成提现时PayTransactionId不能为空
/// *For any* withdrawal completion, the PayTransactionId SHALL NOT be empty.
///
/// **Validates: Requirements 16.6**
/// </summary>
[Property(MaxTest = 50)]
public bool WithdrawalStatusTransition_CompleteRequiresPayTransactionId(PositiveInt seed)
{
// Arrange: 创建处理中状态的提现记录
using var dbContext = CreateDbContext();
var (withdrawalId, _) = CreateTestWithdrawal(dbContext, seed.Get, status: 2);
var service = new DistributionService(dbContext, _mockLogger.Object);
var request = new CompleteWithdrawalRequest
{
Id = withdrawalId,
PayTransactionId = "" // 空的交易号
};
// Act & Assert: 完成应该抛出异常
try
{
service.CompleteWithdrawalAsync(request).GetAwaiter().GetResult();
return false; // 不应该成功
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.ParamError;
}
}
#endregion
#region
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new AdminBusinessDbContext(options);
}
/// <summary>
/// 创建测试提现记录
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="seed">随机种子</param>
/// <param name="status">提现状态</param>
/// <returns>提现记录ID和用户ID</returns>
private (long WithdrawalId, long UserId) CreateTestWithdrawal(AdminBusinessDbContext dbContext, int seed, int status)
{
// 先创建用户
var initialBalance = (seed % 1000) + 100m; // 100-1099
var withdrawalAmount = (seed % 100) + 10m; // 10-109
var user = new User
{
Uid = $"{seed % 1000000:D6}",
OpenId = $"openid_{seed}",
Phone = $"138{seed % 100000000:D8}",
Nickname = $"User_{seed}",
UserLevel = (seed % 3) + 1, // 1-3
Balance = initialBalance - withdrawalAmount, // 提现后的余额
TotalIncome = initialBalance + 500m,
WithdrawnAmount = withdrawalAmount, // 已提现金额
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Users.Add(user);
dbContext.SaveChanges();
// 创建提现记录
var withdrawal = new Withdrawal
{
WithdrawalNo = $"WD{DateTime.Now:yyyyMMddHHmmss}{seed % 10000:D4}",
UserId = user.Id,
Amount = withdrawalAmount,
BeforeBalance = initialBalance,
AfterBalance = initialBalance - withdrawalAmount,
Status = status,
AuditUserId = status >= 2 ? (long?)(seed % 1000 + 1) : null,
AuditTime = status >= 2 ? DateTime.Now.AddMinutes(-seed % 60) : null,
AuditRemark = status == 4 ? $"拒绝原因_{seed}" : null,
PayTime = status == 3 ? DateTime.Now.AddMinutes(-seed % 30) : null,
PayTransactionId = status == 3 ? $"TXN{seed % 1000000:D6}" : null,
CreateTime = DateTime.Now.AddDays(-seed % 30),
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Withdrawals.Add(withdrawal);
dbContext.SaveChanges();
return (withdrawal.Id, user.Id);
}
#endregion
}