using FsCheck; using FsCheck.Xunit; using HoneyBox.Core.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using HoneyBox.Model.Models.Auth; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; public class UserServicePropertyTests { private HoneyBoxDbContext CreateInMemoryDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new HoneyBoxDbContext(options); } private UserService CreateUserService(HoneyBoxDbContext dbContext) { var mockLogger = new Mock>(); return new UserService(dbContext, mockLogger.Object); } /// /// Property 2: 新用户创建完整性 /// For any valid CreateUserDto, the created user should have all required fields populated: /// - Valid uid (non-empty) /// - Non-empty nickname /// - Valid headimg URL /// - Correct pid if provided /// Validates: Requirements 1.3, 2.3 /// [Property(MaxTest = 100)] public async Task CreatedUserHasAllRequiredFields() { var dbContext = CreateInMemoryDbContext(); var userService = CreateUserService(dbContext); var createDto = new CreateUserDto { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Nickname = "TestUser_" + Random.Shared.Next(1000, 9999), Headimg = "https://example.com/avatar.jpg", Pid = 0, ClickId = null }; var createdUser = await userService.CreateUserAsync(createDto); // Verify all required fields are populated var hasValidUid = !string.IsNullOrWhiteSpace(createdUser.Uid); var hasNickname = !string.IsNullOrWhiteSpace(createdUser.Nickname); var hasHeadImg = !string.IsNullOrWhiteSpace(createdUser.HeadImg); var hasCorrectPid = createdUser.Pid == 0; return hasValidUid && hasNickname && hasHeadImg && hasCorrectPid; } /// /// Property 9: 用户信息完整性 /// For any user, GetUserInfoAsync should return complete information including: /// - All required fields (id, uid, nickname, headimg, mobile, money, integral, vip) /// - Masked mobile number in format XXX****XXXX /// - Correct MobileIs flag (0 if no mobile, 1 if mobile exists) /// Validates: Requirements 4.1, 4.4 /// [Property(MaxTest = 100)] public async Task UserInfoIsComplete(NonEmptyString openId) { var dbContext = CreateInMemoryDbContext(); var userService = CreateUserService(dbContext); var createDto = new CreateUserDto { OpenId = openId.Item, Nickname = "TestUser", Headimg = "https://example.com/avatar.jpg", Mobile = "13812345678", Pid = 0 }; var createdUser = await userService.CreateUserAsync(createDto); var userInfo = await userService.GetUserInfoAsync(createdUser.Id); // Verify all required fields are present var hasId = userInfo?.Id > 0; var hasUid = !string.IsNullOrWhiteSpace(userInfo?.Uid); var hasNickname = !string.IsNullOrWhiteSpace(userInfo?.Nickname); var hasHeadimg = !string.IsNullOrWhiteSpace(userInfo?.Headimg); var hasMobileInfo = userInfo?.MobileIs == 1; var hasMaskedMobile = userInfo?.Mobile != null && userInfo.Mobile.Contains("****"); return hasId && hasUid && hasNickname && hasHeadimg && hasMobileInfo && hasMaskedMobile; } /// /// Property 10: 昵称更新验证 /// For any user, updating with a valid (non-empty) nickname should succeed, /// and updating with empty/whitespace nickname should not change the nickname. /// Validates: Requirements 4.2 /// [Property(MaxTest = 100)] public async Task NicknameUpdateValidation() { var dbContext = CreateInMemoryDbContext(); var userService = CreateUserService(dbContext); var createDto = new CreateUserDto { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Nickname = "OriginalNickname", Headimg = "https://example.com/avatar.jpg", Pid = 0 }; var createdUser = await userService.CreateUserAsync(createDto); var originalNickname = createdUser.Nickname; // Update with valid nickname var newNickname = "UpdatedNickname_" + Random.Shared.Next(1000, 9999); var updateDto = new UpdateUserDto { Nickname = newNickname }; await userService.UpdateUserAsync(createdUser.Id, updateDto); var updatedUser = await userService.GetUserByIdAsync(createdUser.Id); // Verify nickname was updated return updatedUser?.Nickname == newNickname && updatedUser.Nickname != originalNickname; } /// /// Property 11: VIP等级计算 /// For any user with a given spending amount, CalculateVipLevelAsync should return /// a VIP level that matches the spending threshold configured in VipLevel table. /// Validates: Requirements 4.5 /// [Property(MaxTest = 100)] public async Task VipLevelCalculationIsCorrect(PositiveInt spendingAmount) { var dbContext = CreateInMemoryDbContext(); var userService = CreateUserService(dbContext); // Setup VIP levels var vipLevels = new[] { new VipLevel { Level = 1, Title = "Bronze", Number = 100, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, new VipLevel { Level = 2, Title = "Silver", Number = 500, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, new VipLevel { Level = 3, Title = "Gold", Number = 1000, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } }; await dbContext.VipLevels.AddRangeAsync(vipLevels); await dbContext.SaveChangesAsync(); // Create user with spending var createDto = new CreateUserDto { OpenId = "openid123", Nickname = "TestUser", Headimg = "https://example.com/avatar.jpg", Pid = 0 }; var createdUser = await userService.CreateUserAsync(createDto); createdUser.Money = spendingAmount.Item; dbContext.Users.Update(createdUser); await dbContext.SaveChangesAsync(); // Calculate VIP level var calculatedVip = await userService.CalculateVipLevelAsync(createdUser.Id, 0); // Verify VIP level matches spending int expectedVip = 0; if (spendingAmount.Item >= 1000) expectedVip = 3; else if (spendingAmount.Item >= 500) expectedVip = 2; else if (spendingAmount.Item >= 100) expectedVip = 1; return calculatedVip == expectedVip; } }