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; /// /// Withdrawal 属性测试 /// 验证提现审核服务的正确性属性 /// public class WithdrawalPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 15: Withdrawal Status Transitions /// /// Property 15: 审批通过将提现状态从待审核(1)转换为处理中(2) /// *For any* withdrawal approval, the Status SHALL transition from 1 (pending) /// to 2 (processing). /// /// **Validates: Requirements 16.4** /// [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; } /// /// Property 15: 审批通过时记录AuditUserId /// *For any* withdrawal approval, the AuditUserId SHALL be recorded. /// /// **Validates: Requirements 16.4** /// [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; } /// /// Property 15: 审批通过时记录AuditTime /// *For any* withdrawal approval, the AuditTime SHALL be recorded. /// /// **Validates: Requirements 16.4** /// [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; } /// /// Property 15: 拒绝将提现状态从待审核(1)转换为已拒绝(4) /// *For any* withdrawal rejection, the Status SHALL transition from 1 (pending) /// to 4 (cancelled). /// /// **Validates: Requirements 16.5** /// [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; } /// /// Property 15: 拒绝时记录AuditRemark /// *For any* withdrawal rejection, the AuditRemark SHALL be recorded. /// /// **Validates: Requirements 16.5** /// [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; } /// /// Property 15: 拒绝时回滚用户余额 /// *For any* withdrawal rejection, the user's Balance SHALL be restored. /// /// **Validates: Requirements 16.7** /// [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; } /// /// Property 15: 完成提现将状态从处理中(2)转换为已完成(3) /// *For any* withdrawal completion, the Status SHALL transition from 2 (processing) /// to 3 (completed). /// /// **Validates: Requirements 16.6** /// [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; } /// /// Property 15: 完成提现时记录PayTime /// *For any* withdrawal completion, the PayTime SHALL be recorded. /// /// **Validates: Requirements 16.6** /// [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; } /// /// Property 15: 完成提现时记录PayTransactionId /// *For any* withdrawal completion, the PayTransactionId SHALL be recorded. /// /// **Validates: Requirements 16.6** /// [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; } /// /// Property 15: 非待审核状态的提现不能审批通过 /// *For any* withdrawal with Status not equal to 1, approve operation SHALL fail. /// /// **Validates: Requirements 16.4** /// [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; } } /// /// Property 15: 非待审核状态的提现不能拒绝 /// *For any* withdrawal with Status not equal to 1, reject operation SHALL fail. /// /// **Validates: Requirements 16.5** /// [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; } } /// /// Property 15: 非处理中状态的提现不能完成 /// *For any* withdrawal with Status not equal to 2, complete operation SHALL fail. /// /// **Validates: Requirements 16.6** /// [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; } } /// /// Property 15: 拒绝提现时AuditRemark不能为空 /// *For any* withdrawal rejection, the AuditRemark SHALL NOT be empty. /// /// **Validates: Requirements 16.5** /// [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; } } /// /// Property 15: 完成提现时PayTransactionId不能为空 /// *For any* withdrawal completion, the PayTransactionId SHALL NOT be empty. /// /// **Validates: Requirements 16.6** /// [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 辅助方法 /// /// 创建内存数据库上下文 /// private AdminBusinessDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) .Options; return new AdminBusinessDbContext(options); } /// /// 创建测试提现记录 /// /// 数据库上下文 /// 随机种子 /// 提现状态 /// 提现记录ID和用户ID 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 }