using FsCheck; using FsCheck.Xunit; using MiAssessment.Admin.Business.Models.User; using MiAssessment.Admin.Business.Services; using MiAssessment.Model.Data; using MiAssessment.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// UserBusinessService 属性测试 /// public class UserBusinessServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 4: User List Pagination Consistency /// /// **Feature: admin-business-migration, Property 4: User List Pagination Consistency** /// For any valid page and pageSize, the returned list should have at most pageSize items, /// and the total count should be consistent across pages. /// Validates: Requirements 4.1 /// [Property(MaxTest = 50)] public bool UserListPagination_ShouldReturnCorrectPageSize(PositiveInt seed) { var userCount = (seed.Get % 20) + 5; // 5 to 24 users var pageSize = (seed.Get % 5) + 1; // 1 to 5 per page var page = (seed.Get % 3) + 1; // page 1 to 3 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 consistency var expectedItemsOnPage = Math.Min(pageSize, Math.Max(0, userCount - (page - 1) * pageSize)); return result.Total == userCount && result.List.Count <= pageSize && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: admin-business-migration, Property 4: User List Pagination Consistency** /// The total count should remain consistent regardless of which page is requested. /// Validates: Requirements 4.1 /// [Property(MaxTest = 50)] public bool UserListPagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed) { var userCount = (seed.Get % 15) + 10; // 10 to 24 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; } #endregion #region Property 5: User List Filter Accuracy /// /// **Feature: admin-business-migration, Property 5: User List Filter Accuracy** /// When filtering by nickname, all returned users should contain the filter string in their nickname. /// Validates: Requirements 4.3 /// [Property(MaxTest = 50)] public bool UserListFilter_ByNickname_ShouldReturnMatchingUsers(PositiveInt seed) { var filterNames = new[] { "Alice", "Bob", "Charlie", "David" }; var filterName = filterNames[seed.Get % filterNames.Length]; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create users with different nicknames dbContext.Users.Add(CreateTestUser("Alice")); dbContext.Users.Add(CreateTestUser("AliceSmith")); dbContext.Users.Add(CreateTestUser("Bob")); dbContext.Users.Add(CreateTestUser("BobJones")); dbContext.Users.Add(CreateTestUser("Charlie")); dbContext.Users.Add(CreateTestUser("David")); dbContext.SaveChanges(); var request = new UserListRequest { Nickname = filterName }; var result = service.GetUserListAsync(request).GetAwaiter().GetResult(); // All returned users should contain the filter string return result.List.All(u => u.Nickname != null && u.Nickname.Contains(filterName)); } /// /// **Feature: admin-business-migration, Property 5: User List Filter Accuracy** /// When filtering by parent_id, all returned users should have that parent_id. /// Validates: Requirements 4.3 /// [Property(MaxTest = 50)] public bool UserListFilter_ByParentId_ShouldReturnSubordinates(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; // 1 to 5 subordinates 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 6: User Balance Change Audit Trail /// /// **Feature: admin-business-migration, Property 6: User Balance Change Audit Trail** /// For any balance change operation, a corresponding record should be created in the profit table. /// Validates: Requirements 4.4 /// [Property(MaxTest = 50)] public bool UserBalanceChange_ShouldCreateAuditRecord(PositiveInt seed) { var amount = (seed.Get % 100) + 1; // 1 to 100 var isAdd = seed.Get % 2 == 0; using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user with sufficient balance var user = CreateTestUser("TestUser"); user.Money = 1000; // Ensure enough balance for subtraction dbContext.Users.Add(user); dbContext.SaveChanges(); var request = new UserMoneyChangeRequest { Type = MoneyChangeType.Balance, Amount = amount, Operation = isAdd ? OperationType.Add : OperationType.Subtract, Remark = "Property test" }; var result = service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult(); if (!result) return false; // Verify audit record was created var auditRecord = dbContext.ProfitMoneys.FirstOrDefault(p => p.UserId == user.Id); return auditRecord != null && auditRecord.ChangeMoney == (isAdd ? amount : -amount); } /// /// **Feature: admin-business-migration, Property 6: User Balance Change Audit Trail** /// After a balance change, the user's balance should reflect the change accurately. /// Validates: Requirements 4.4 /// [Property(MaxTest = 50)] public bool UserBalanceChange_ShouldUpdateBalanceAccurately(PositiveInt seed) { var initialBalance = (seed.Get % 500) + 100; // 100 to 599 var changeAmount = (seed.Get % 50) + 1; // 1 to 50 var isAdd = seed.Get % 2 == 0; 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 = changeAmount, Operation = isAdd ? OperationType.Add : OperationType.Subtract }; service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult(); // Refresh user from database var updatedUser = dbContext.Users.Find(user.Id); var expectedBalance = isAdd ? initialBalance + changeAmount : initialBalance - changeAmount; return updatedUser!.Money == expectedBalance; } #endregion #region Property 7: User Status Toggle Consistency /// /// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency** /// Toggling user status (ban/unban) should correctly update the status field. /// Validates: Requirements 4.5, 4.6 /// [Property(MaxTest = 50)] public bool UserStatusToggle_ShouldUpdateStatusCorrectly(PositiveInt seed) { var initialStatus = (byte)(seed.Get % 2); // 0 or 1 var newStatus = (byte)(1 - initialStatus); // Toggle using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.Status = initialStatus; dbContext.Users.Add(user); dbContext.SaveChanges(); // Toggle status var result = service.SetUserStatusAsync(user.Id, newStatus).GetAwaiter().GetResult(); if (!result) return false; // Verify status was updated var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Status == newStatus; } /// /// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency** /// Setting the same status multiple times should be idempotent. /// Validates: Requirements 4.5, 4.6 /// [Property(MaxTest = 50)] public bool UserStatusToggle_ShouldBeIdempotent(PositiveInt seed) { var status = (byte)(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 = status; dbContext.Users.Add(user); dbContext.SaveChanges(); // Set same status multiple times service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult(); service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult(); service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult(); // Verify status remains the same var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.Status == status; } /// /// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency** /// Test account flag toggle should correctly update the is_test field. /// Validates: Requirements 4.7 /// [Property(MaxTest = 50)] public bool TestAccountToggle_ShouldUpdateFlagCorrectly(PositiveInt seed) { var initialIsTest = seed.Get % 2; // 0 or 1 var newIsTest = 1 - initialIsTest; // Toggle using var dbContext = CreateDbContext(); var service = new UserBusinessService(dbContext, _mockLogger.Object); // Create test user var user = CreateTestUser("TestUser"); user.IsTest = initialIsTest; dbContext.Users.Add(user); dbContext.SaveChanges(); // Toggle test account flag var result = service.SetTestAccountAsync(user.Id, newIsTest).GetAwaiter().GetResult(); if (!result) return false; // Verify flag was updated var updatedUser = dbContext.Users.Find(user.Id); return updatedUser!.IsTest == newIsTest; } #endregion #region Helper Methods private MiAssessmentDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new MiAssessmentDbContext(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 }