using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Business.Models; using HoneyBox.Admin.Business.Models.User; using HoneyBox.Admin.Business.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// 用户管理前端模块属性测试 /// Feature: user-management-frontend /// public class UserManagementFrontendPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 1: 搜索参数正确传递 /// /// **Feature: user-management-frontend, Property 1: 搜索参数正确传递** /// For any user list search request, when the admin inputs search conditions, /// the API call query parameters should exactly match the user input search conditions. /// **Validates: Requirements 1.2** /// [Property(MaxTest = 100)] public bool SearchParameters_ShouldFilterCorrectly_ByNickname(PositiveInt seed) { var nicknames = new[] { "Alice", "Bob", "Charlie", "David", "Eve" }; var searchNickname = nicknames[seed.Get % nicknames.Length]; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create users with different nicknames foreach (var name in nicknames) { dbContext.Users.Add(CreateTestUser(name)); dbContext.Users.Add(CreateTestUser($"{name}Smith")); } dbContext.SaveChanges(); var request = new UserListRequest { Nickname = searchNickname }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // All returned users should contain the search nickname return result.List.All(u => u.Nickname != null && u.Nickname.Contains(searchNickname)); } /// /// **Feature: user-management-frontend, Property 1: 搜索参数正确传递** /// For any user list search request with mobile filter, /// all returned users should have matching mobile numbers. /// **Validates: Requirements 1.2** /// [Property(MaxTest = 100)] public bool SearchParameters_ShouldFilterCorrectly_ByMobile(PositiveInt seed) { var mobilePrefix = $"138{(seed.Get % 100):D2}"; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create users with different mobile numbers for (int i = 0; i < 10; i++) { var user = CreateTestUser($"User{i}"); user.Mobile = i < 5 ? $"{mobilePrefix}{i:D6}" : $"139{i:D8}"; dbContext.Users.Add(user); } dbContext.SaveChanges(); var request = new UserListRequest { Mobile = mobilePrefix }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // All returned users should have mobile containing the prefix return result.List.All(u => u.Mobile != null && u.Mobile.Contains(mobilePrefix)); } /// /// **Feature: user-management-frontend, Property 1: 搜索参数正确传递** /// For any user list search request with parent ID filter, /// all returned users should have the specified parent ID. /// **Validates: Requirements 1.2** /// [Property(MaxTest = 100)] public bool SearchParameters_ShouldFilterCorrectly_ByParentId(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create parent user var parent = CreateTestUser("Parent"); dbContext.Users.Add(parent); dbContext.SaveChanges(); // Create subordinate users var subordinateCount = (seed.Get % 5) + 1; for (int i = 0; i < subordinateCount; i++) { var child = CreateTestUser($"Child{i}"); child.Pid = parent.Id; dbContext.Users.Add(child); } // Create other users without parent for (int i = 0; i < 3; i++) { dbContext.Users.Add(CreateTestUser($"Other{i}")); } dbContext.SaveChanges(); var request = new UserListRequest { ParentId = parent.Id }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // All returned users should have the specified parent ID return result.Total == subordinateCount && result.List.All(u => u.ParentId == parent.Id); } #endregion #region Property 2: 分页参数正确传递 /// /// **Feature: user-management-frontend, Property 2: 分页参数正确传递** /// For any pagination request, the returned list should have at most pageSize items, /// and the page and pageSize in response should match the request. /// **Validates: Requirements 1.4** /// [Property(MaxTest = 100)] public bool PaginationParameters_ShouldReturnCorrectPageSize(PositiveInt seed) { var userCount = (seed.Get % 30) + 10; // 10 to 39 users var pageSize = (seed.Get % 10) + 1; // 1 to 10 per page var page = (seed.Get % 5) + 1; // page 1 to 5 using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test users for (int i = 0; i < userCount; i++) { dbContext.Users.Add(CreateTestUser($"User{i}")); } dbContext.SaveChanges(); var request = new UserListRequest { Page = page, PageSize = pageSize }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // Verify pagination parameters are correctly passed return result.Total == userCount && result.List.Count <= pageSize && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: user-management-frontend, Property 2: 分页参数正确传递** /// The total count should remain consistent regardless of which page is requested. /// **Validates: Requirements 1.4** /// [Property(MaxTest = 100)] public bool PaginationParameters_TotalShouldBeConsistentAcrossPages(PositiveInt seed) { var userCount = (seed.Get % 20) + 15; // 15 to 34 users var pageSize = 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test users for (int i = 0; i < userCount; i++) { dbContext.Users.Add(CreateTestUser($"User{i}")); } dbContext.SaveChanges(); // Get multiple pages var page1 = service.GetUserListAsync(new UserListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetUserListAsync(new UserListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); var page3 = service.GetUserListAsync(new UserListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult(); // Total should be consistent across all pages return page1.Total == page2.Total && page2.Total == page3.Total && page1.Total == userCount; } /// /// **Feature: user-management-frontend, Property 2: 分页参数正确传递** /// Different pages should return different users (no overlap). /// **Validates: Requirements 1.4** /// [Property(MaxTest = 100)] public bool PaginationParameters_DifferentPagesShouldNotOverlap(PositiveInt seed) { var userCount = (seed.Get % 15) + 20; // 20 to 34 users var pageSize = 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test users for (int i = 0; i < userCount; i++) { dbContext.Users.Add(CreateTestUser($"User{i}")); } dbContext.SaveChanges(); // Get first two pages var page1 = service.GetUserListAsync(new UserListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetUserListAsync(new UserListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); // User IDs should not overlap between pages var page1Ids = page1.List.Select(u => u.Id).ToHashSet(); var page2Ids = page2.List.Select(u => u.Id).ToHashSet(); return !page1Ids.Overlaps(page2Ids); } #endregion #region Property 3: 资金变动参数验证 /// /// **Feature: user-management-frontend, Property 3: 资金变动参数验证** /// When operation type is "subtract" and amount is greater than user's current balance, /// the system should throw an exception instead of executing the subtraction. /// **Validates: Requirements 2.3** /// [Property(MaxTest = 100)] public bool MoneyChangeValidation_ShouldRejectInsufficientBalance(PositiveInt seed) { var initialBalance = (seed.Get % 100) + 10; // 10 to 109 var subtractAmount = initialBalance + (seed.Get % 50) + 1; // Always more than balance using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user with limited balance var user = CreateTestUser("TestUser"); user.Money = initialBalance; dbContext.Users.Add(user); dbContext.SaveChanges(); var request = new UserMoneyChangeRequest { Type = MoneyChangeType.Balance, Amount = subtractAmount, Operation = OperationType.Subtract, Remark = "Property test - insufficient balance" }; try { service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { // Verify user balance was not changed var updatedUser = dbContext.Users.Find(user.Id); return ex.Message.Contains("余额不足") && updatedUser!.Money == initialBalance; } } /// /// **Feature: user-management-frontend, Property 3: 资金变动参数验证** /// When operation type is "add", the balance should increase by exactly the specified amount. /// **Validates: Requirements 2.3** /// [Property(MaxTest = 100)] public bool MoneyChangeValidation_AddShouldIncreaseBalanceExactly(PositiveInt seed) { var initialBalance = (seed.Get % 500) + 100; // 100 to 599 var addAmount = (seed.Get % 100) + 1; // 1 to 100 using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.Money = initialBalance; dbContext.Users.Add(user); dbContext.SaveChanges(); var request = new UserMoneyChangeRequest { Type = MoneyChangeType.Balance, Amount = addAmount, Operation = OperationType.Add, Remark = "Property test - add balance" }; service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult(); // Refresh user from database var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Money == initialBalance + addAmount; } /// /// **Feature: user-management-frontend, Property 3: 资金变动参数验证** /// When operation type is "subtract" and amount is less than or equal to balance, /// the balance should decrease by exactly the specified amount. /// **Validates: Requirements 2.3** /// [Property(MaxTest = 100)] public bool MoneyChangeValidation_SubtractShouldDecreaseBalanceExactly(PositiveInt seed) { var initialBalance = (seed.Get % 500) + 200; // 200 to 699 var subtractAmount = (seed.Get % 100) + 1; // 1 to 100 (always less than balance) using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.Money = initialBalance; dbContext.Users.Add(user); dbContext.SaveChanges(); var request = new UserMoneyChangeRequest { Type = MoneyChangeType.Balance, Amount = subtractAmount, Operation = OperationType.Subtract, Remark = "Property test - subtract balance" }; service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult(); // Refresh user from database var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Money == initialBalance - subtractAmount; } #endregion #region Property 4: 用户状态切换一致性 /// /// **Feature: user-management-frontend, Property 4: 用户状态切换一致性** /// Ban operation should set status to 0, unban operation should set status to 1, /// and the user list should display the correct status after the operation. /// **Validates: Requirements 3.1, 3.2** /// [Property(MaxTest = 100)] public bool UserStatusToggle_BanShouldSetStatusToZero(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user with status 1 (active) var user = CreateTestUser("TestUser"); user.Status = 1; dbContext.Users.Add(user); dbContext.SaveChanges(); // Ban the user (set status to 0) var result = service.SetUserStatusAsync(user.Id, 0).GetAwaiter().GetResult(); if (!result) return false; // Verify status was set to 0 var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Status == 0; } /// /// **Feature: user-management-frontend, Property 4: 用户状态切换一致性** /// Unban operation should set status to 1. /// **Validates: Requirements 3.1, 3.2** /// [Property(MaxTest = 100)] public bool UserStatusToggle_UnbanShouldSetStatusToOne(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user with status 0 (banned) var user = CreateTestUser("TestUser"); user.Status = 0; dbContext.Users.Add(user); dbContext.SaveChanges(); // Unban the user (set status to 1) var result = service.SetUserStatusAsync(user.Id, 1).GetAwaiter().GetResult(); if (!result) return false; // Verify status was set to 1 var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Status == 1; } /// /// **Feature: user-management-frontend, Property 4: 用户状态切换一致性** /// Status toggle should be idempotent - setting the same status multiple times /// should result in the same final state. /// **Validates: Requirements 3.1, 3.2** /// [Property(MaxTest = 100)] public bool UserStatusToggle_ShouldBeIdempotent(PositiveInt seed) { var targetStatus = seed.Get % 2; // 0 or 1 using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.Status = (byte)(1 - targetStatus); // Start with opposite status dbContext.Users.Add(user); dbContext.SaveChanges(); // Set status multiple times service.SetUserStatusAsync(user.Id, targetStatus).GetAwaiter().GetResult(); service.SetUserStatusAsync(user.Id, targetStatus).GetAwaiter().GetResult(); service.SetUserStatusAsync(user.Id, targetStatus).GetAwaiter().GetResult(); // Verify status is correct var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Status == targetStatus; } /// /// **Feature: user-management-frontend, Property 4: 用户状态切换一致性** /// User list should reflect the correct status after status change. /// **Validates: Requirements 3.1, 3.2** /// [Property(MaxTest = 100)] public bool UserStatusToggle_ListShouldReflectCorrectStatus(PositiveInt seed) { var targetStatus = seed.Get % 2; // 0 or 1 using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.Status = (byte)(1 - targetStatus); dbContext.Users.Add(user); dbContext.SaveChanges(); // Change status service.SetUserStatusAsync(user.Id, targetStatus).GetAwaiter().GetResult(); // Get user list and verify status var request = new UserListRequest { UserId = user.Id }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); return result.List.Count == 1 && result.List[0].Status == targetStatus; } #endregion #region Property 5: 盈亏计算正确性 /// /// **Feature: user-management-frontend, Property 5: 盈亏计算正确性** /// Profit/loss amount should equal: User payment - Shipping amount - Backpack amount - Remaining DaDa coupons, /// and profit status should correctly display based on whether the amount is positive or negative. /// **Validates: Requirements 6.4** /// [Property(MaxTest = 100)] public bool ProfitLossCalculation_ShouldBeCorrect(PositiveInt seed) { // Generate test data var useMoney = (seed.Get % 1000) + 100m; // 100 to 1099 var fhMoney = (seed.Get % 500) + 50m; // 50 to 549 var bbMoney = (seed.Get % 300) + 20m; // 20 to 319 var syMoney = (seed.Get % 100) + 10m; // 10 to 109 // Calculate expected profit/loss var expectedYueMoney = useMoney - fhMoney - bbMoney - syMoney; var expectedStatus = expectedYueMoney >= 0 ? "盈利" : "亏损"; // Create a ProfitLossItem and verify calculation var item = new ProfitLossItem { UserId = 1, Uid = "TEST001", Nickname = "TestUser", UseMoney = useMoney, FhMoney = fhMoney, BbMoney = bbMoney, SyMoney = syMoney, YueMoney = expectedYueMoney, ProfitStatus = expectedStatus }; // Verify the calculation formula var calculatedYueMoney = item.UseMoney - item.FhMoney - item.BbMoney - item.SyMoney; var calculatedStatus = calculatedYueMoney >= 0 ? "盈利" : "亏损"; return item.YueMoney == calculatedYueMoney && item.ProfitStatus == calculatedStatus; } /// /// **Feature: user-management-frontend, Property 5: 盈亏计算正确性** /// When user payment equals total deductions, profit/loss should be zero and status should be "盈利". /// **Validates: Requirements 6.4** /// [Property(MaxTest = 100)] public bool ProfitLossCalculation_ZeroShouldBeProfit(PositiveInt seed) { var useMoney = (seed.Get % 1000) + 100m; var fhMoney = useMoney / 3; var bbMoney = useMoney / 3; var syMoney = useMoney - fhMoney - bbMoney; // Make total equal to useMoney var yueMoney = useMoney - fhMoney - bbMoney - syMoney; var status = yueMoney >= 0 ? "盈利" : "亏损"; // Zero or positive should be "盈利" return yueMoney == 0 && status == "盈利"; } /// /// **Feature: user-management-frontend, Property 5: 盈亏计算正确性** /// When deductions exceed payment, profit/loss should be negative and status should be "亏损". /// **Validates: Requirements 6.4** /// [Property(MaxTest = 100)] public bool ProfitLossCalculation_NegativeShouldBeLoss(PositiveInt seed) { var useMoney = (seed.Get % 100) + 50m; // 50 to 149 var fhMoney = useMoney + (seed.Get % 50) + 10m; // Always more than useMoney var bbMoney = 0m; var syMoney = 0m; var yueMoney = useMoney - fhMoney - bbMoney - syMoney; var status = yueMoney >= 0 ? "盈利" : "亏损"; // Negative should be "亏损" return yueMoney < 0 && status == "亏损"; } #endregion #region Property 6: API响应格式一致性 /// /// **Feature: user-management-frontend, Property 6: API响应格式一致性** /// For any backend API response, the response format should conform to the unified ApiResponse structure. /// **Validates: Requirements 10.1-10.10** /// [Property(MaxTest = 100)] public bool ApiResponseFormat_PagedResultShouldHaveConsistentStructure(PositiveInt seed) { var userCount = (seed.Get % 20) + 5; var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test users for (int i = 0; i < userCount; i++) { dbContext.Users.Add(CreateTestUser($"User{i}")); } dbContext.SaveChanges(); var request = new UserListRequest { Page = page, PageSize = pageSize }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize && result.TotalPages == (int)Math.Ceiling((double)result.Total / result.PageSize); } /// /// **Feature: user-management-frontend, Property 6: API响应格式一致性** /// User box API should return consistent PagedResult structure. /// **Validates: Requirements 10.1** /// [Property(MaxTest = 100)] public bool ApiResponseFormat_UserBoxShouldHaveConsistentStructure(PositiveInt seed) { var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); dbContext.Users.Add(user); dbContext.SaveChanges(); var query = new UserBoxQuery { Page = page, PageSize = pageSize }; var result = service.GetUserBoxAsync(user.Id, query).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: user-management-frontend, Property 6: API响应格式一致性** /// User orders API should return consistent PagedResult structure. /// **Validates: Requirements 10.2** /// [Property(MaxTest = 100)] public bool ApiResponseFormat_UserOrdersShouldHaveConsistentStructure(PositiveInt seed) { var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); dbContext.Users.Add(user); dbContext.SaveChanges(); var query = new UserOrderQuery { Page = page, PageSize = pageSize }; var result = service.GetUserOrdersAsync(user.Id, query).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: user-management-frontend, Property 6: API响应格式一致性** /// Money detail API should return consistent PagedResult structure. /// **Validates: Requirements 10.3** /// [Property(MaxTest = 100)] public bool ApiResponseFormat_MoneyDetailShouldHaveConsistentStructure(PositiveInt seed) { var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); dbContext.Users.Add(user); dbContext.SaveChanges(); var query = new MoneyDetailQuery { Page = page, PageSize = pageSize }; var result = service.GetUserMoneyDetailAsync(user.Id, query).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: user-management-frontend, Property 6: API响应格式一致性** /// Login stats API should return consistent LoginStatsResponse structure. /// **Validates: Requirements 10.6** /// [Property(MaxTest = 100)] public bool ApiResponseFormat_LoginStatsShouldHaveConsistentStructure(PositiveInt seed) { var types = new[] { "day", "week", "month" }; var type = types[seed.Get % types.Length]; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); var query = new LoginStatsQuery { Type = type }; var result = service.GetLoginStatsAsync(query).GetAwaiter().GetResult(); // Verify LoginStatsResponse structure return result != null && result.Labels != null && result.Values != null && result.Labels.Count == result.Values.Count && result.TotalLogins >= 0; } #endregion #region Helper Methods private HoneyBoxDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } private User CreateTestUser(string nickname) { return new User { OpenId = Guid.NewGuid().ToString("N"), Uid = $"UID{DateTime.Now.Ticks}{Guid.NewGuid():N}".Substring(0, 20), Nickname = nickname, HeadImg = "https://example.com/avatar.png", Mobile = $"138{new Random().Next(10000000, 99999999)}", Money = 100, Integral = 50, Score = 30, Status = 1, IsTest = 0, Pid = 0, Vip = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; } #endregion }