using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Business.Models.Finance; using HoneyBox.Admin.Business.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// FinanceService 属性测试 /// public class FinanceServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 13: Consumption Ranking Sort Order /// /// **Feature: admin-business-migration, Property 13: Consumption Ranking Sort Order** /// For any consumption ranking request, the returned users should be sorted /// by total consumption in descending order. /// Validates: Requirements 7.1 /// [Property(MaxTest = 100)] public bool ConsumptionRanking_ShouldBeSortedByTotalConsumptionDescending(PositiveInt userCount, PositiveInt orderCount) { var actualUserCount = (userCount.Get % 10) + 2; // 2-11 users var actualOrderCount = (orderCount.Get % 30) + 10; // 10-39 orders var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new FinanceService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, actualUserCount); // Seed orders with random amounts for random users var random = new Random(userCount.Get); for (int i = 0; i < actualOrderCount; i++) { var userId = random.Next(1, actualUserCount + 1); var price = random.Next(10, 1000); dbContext.Orders.Add(CreateOrder(userId, $"ORD{i:D5}", price)); } dbContext.SaveChanges(); // Get consumption ranking var request = new FinanceQueryRequest { Page = 1, PageSize = 100 }; var result = service.GetConsumptionRankingAsync(request).GetAwaiter().GetResult(); // Verify sorted by total consumption descending for (int i = 0; i < result.List.Count - 1; i++) { if (result.List[i].TotalConsumption < result.List[i + 1].TotalConsumption) { return false; } } return true; } /// /// **Feature: admin-business-migration, Property 13: Consumption Ranking Sort Order** /// For any consumption ranking, the total consumption should equal the sum of /// WeChat, balance, and integral payments. /// Validates: Requirements 7.1, 7.2 /// [Property(MaxTest = 100)] public bool ConsumptionRanking_TotalShouldEqualSumOfPayments(PositiveInt seed) { var userCount = (seed.Get % 5) + 2; // 2-6 users var orderCount = (seed.Get % 20) + 5; // 5-24 orders var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new FinanceService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, userCount); // Seed orders var random = new Random(seed.Get); for (int i = 0; i < orderCount; i++) { var userId = random.Next(1, userCount + 1); var price = random.Next(100, 500); var useMoney = random.Next(0, price / 3); var useIntegral = random.Next(0, price / 3); dbContext.Orders.Add(new Order { UserId = userId, OrderNum = $"ORD{i:D5}", Price = price, UseMoney = useMoney, UseIntegral = useIntegral, UseScore = 0, Status = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, GoodsId = 1, GoodsTitle = "商品", GoodsPrice = price, OrderTotal = price, OrderZheTotal = price, Zhe = 1, Num = 1, PrizeNum = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() }); } dbContext.SaveChanges(); // Get consumption ranking var request = new FinanceQueryRequest { Page = 1, PageSize = 100 }; var result = service.GetConsumptionRankingAsync(request).GetAwaiter().GetResult(); // Verify total equals sum of payments foreach (var item in result.List) { var expectedTotal = item.WeChatPayment + item.BalancePayment + item.IntegralPayment; if (Math.Abs(item.TotalConsumption - expectedTotal) > 0.01m) { return false; } } return true; } #endregion #region Property 14: Financial Query Filter Accuracy /// /// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy** /// For any balance detail query with user ID filter, all returned records /// should belong to the specified user. /// Validates: Requirements 7.7 /// [Property(MaxTest = 100)] public bool BalanceDetails_FilterByUserId_ShouldReturnOnlyMatchingRecords(PositiveInt userId, PositiveInt recordCount) { var actualUserId = (userId.Get % 5) + 1; // 1-5 var actualRecordCount = (recordCount.Get % 20) + 10; // 10-29 records var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new FinanceService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 5); // Seed profit money records for multiple users var random = new Random(userId.Get); for (int i = 0; i < actualRecordCount; i++) { var recordUserId = random.Next(1, 6); // Random user 1-5 dbContext.ProfitMoneys.Add(new ProfitMoney { UserId = recordUserId, ChangeMoney = 100, Money = 100, Type = 1, Content = $"变动{i}", ShareUid = 0, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); // Query with user ID filter var request = new FinanceQueryRequest { UserId = actualUserId, Page = 1, PageSize = 100 }; var result = service.GetBalanceDetailsAsync(request).GetAwaiter().GetResult(); // Verify all returned records belong to the specified user return result.List.All(r => r.UserId == actualUserId); } /// /// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy** /// For any integral detail query with date range filter, all returned records /// should fall within the specified date range. /// Validates: Requirements 7.7 /// [Property(MaxTest = 100)] public bool IntegralDetails_FilterByDateRange_ShouldReturnOnlyMatchingRecords(PositiveInt seed) { var recordCount = (seed.Get % 20) + 10; // 10-29 records var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new FinanceService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 1); // Seed profit integral records with various dates var baseDate = DateTime.Today; for (int i = 0; i < recordCount; i++) { var daysOffset = (i % 21) - 10; // -10 to +10 days dbContext.ProfitIntegrals.Add(new ProfitIntegral { UserId = 1, ChangeMoney = 50, Money = 50, Type = 1, Content = $"积分{i}", ShareUid = 0, CreatedAt = baseDate.AddDays(daysOffset) }); } dbContext.SaveChanges(); // Query with date range filter (last 5 days) var startDate = baseDate.AddDays(-5); var endDate = baseDate; var request = new FinanceQueryRequest { StartDate = startDate, EndDate = endDate, Page = 1, PageSize = 100 }; var result = service.GetIntegralDetailsAsync(request).GetAwaiter().GetResult(); // Verify all returned records fall within the date range var effectiveEndDate = endDate.AddDays(1); return result.List.All(r => r.CreatedAt >= startDate && r.CreatedAt < effectiveEndDate); } /// /// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy** /// For any recharge record query with user ID filter, all returned records /// should belong to the specified user. /// Validates: Requirements 7.7 /// [Property(MaxTest = 100)] public bool RechargeRecords_FilterByUserId_ShouldReturnOnlyMatchingRecords(PositiveInt userId, PositiveInt recordCount) { var actualUserId = (userId.Get % 3) + 1; // 1-3 var actualRecordCount = (recordCount.Get % 15) + 5; // 5-19 records var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new FinanceService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 3); // Seed profit pay records for multiple users var random = new Random(userId.Get); for (int i = 0; i < actualRecordCount; i++) { var recordUserId = random.Next(1, 4); // Random user 1-3 dbContext.ProfitPays.Add(new ProfitPay { UserId = recordUserId, OrderNum = $"PAY{i:D5}", ChangeMoney = 100, Content = $"充值{i}", PayType = 0, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); // Query with user ID filter var request = new FinanceQueryRequest { UserId = actualUserId, Page = 1, PageSize = 100 }; var result = service.GetRechargeRecordsAsync(request).GetAwaiter().GetResult(); // Verify all returned records belong to the specified user return result.List.All(r => r.UserId == actualUserId); } #endregion #region Helper Methods private void SeedUsers(HoneyBoxDbContext dbContext, int count) { for (int i = 1; i <= count; i++) { dbContext.Users.Add(new User { Id = i, Uid = $"U{i:D3}", Nickname = $"测试用户{i}", Mobile = $"1380013800{i}", OpenId = $"openid{i}", HeadImg = $"http://test.com/head{i}.jpg", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); } dbContext.SaveChanges(); } private Order CreateOrder(int userId, string orderNum, decimal price) { return new Order { UserId = userId, OrderNum = orderNum, Price = price, UseMoney = 0, UseIntegral = 0, UseScore = 0, Status = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, GoodsId = 1, GoodsTitle = "商品", GoodsPrice = price, OrderTotal = price, OrderZheTotal = price, Zhe = 1, Num = 1, PrizeNum = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() }; } #endregion }