using FsCheck; using FsCheck.Xunit; using HoneyBox.Core.Interfaces; 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; /// /// AuthService属性测试 /// 测试认证服务的核心属性 /// public class AuthServicePropertyTests { private HoneyBoxDbContext CreateInMemoryDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new HoneyBoxDbContext(options); } private (AuthService authService, Mock wechatMock, Mock redisMock) CreateAuthService(HoneyBoxDbContext dbContext) { var mockUserLogger = new Mock>(); var userService = new UserService(dbContext, mockUserLogger.Object); var jwtSettings = new JwtSettings { Secret = "your-secret-key-must-be-at-least-32-characters-long-for-hs256", Issuer = "HoneyBox", Audience = "HoneyBoxUsers", ExpirationMinutes = 1440, RefreshTokenExpirationDays = 7 }; var mockJwtLogger = new Mock>(); var jwtService = new JwtService(jwtSettings, mockJwtLogger.Object); var mockWechatService = new Mock(); var mockIpLocationService = new Mock(); var mockRedisService = new Mock(); var mockAuthLogger = new Mock>(); // Default IP location result mockIpLocationService.Setup(x => x.GetLocationAsync(It.IsAny())) .ReturnsAsync(new IpLocationResult { Success = true, Province = "北京", City = "北京", Adcode = "110000" }); var authService = new AuthService( dbContext, userService, jwtService, mockWechatService.Object, mockIpLocationService.Object, mockRedisService.Object, mockAuthLogger.Object); return (authService, mockWechatService, mockRedisService); } /// /// Property 1: 微信登录用户查找优先级 /// For any login request with unionid, the system should prioritize finding user by unionid first, /// then by openid if unionid lookup fails. /// Validates: Requirements 1.2 /// Feature: user-auth-migration, Property 1: 微信登录用户查找优先级 /// [Property(MaxTest = 100)] public async Task WechatLoginPrioritizesUnionIdLookup() { var dbContext = CreateInMemoryDbContext(); var (authService, wechatMock, redisMock) = CreateAuthService(dbContext); var openId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8); var unionId = "unionid_" + Guid.NewGuid().ToString().Substring(0, 8); // Setup: Create a user with unionid var existingUser = new User { OpenId = "different_openid", UnionId = unionId, Uid = "uid123", Nickname = "ExistingUser", HeadImg = "https://example.com/avatar.jpg", Status = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await dbContext.Users.AddAsync(existingUser); await dbContext.SaveChangesAsync(); // Mock WeChat API to return the unionid wechatMock.Setup(x => x.GetOpenIdAsync(It.IsAny())) .ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId, UnionId = unionId }); // Mock Redis to allow login (no debounce) redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act var result = await authService.WechatMiniProgramLoginAsync("test_code", null, null); // Assert: Should find existing user by unionid, not create new one var usersCount = await dbContext.Users.CountAsync(); return result.Success && result.UserId == existingUser.Id && usersCount == 1; } /// /// Property 4: 登录防抖机制 /// For any user making repeated login requests within 3 seconds, /// the second and subsequent requests should be rejected with "请勿频繁登录" error. /// Validates: Requirements 1.6, 2.6 /// Feature: user-auth-migration, Property 4: 登录防抖机制 /// [Property(MaxTest = 100)] public async Task LoginDebounceRejectsRepeatedRequests() { var dbContext = CreateInMemoryDbContext(); var (authService, wechatMock, redisMock) = CreateAuthService(dbContext); var openId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8); // Mock WeChat API wechatMock.Setup(x => x.GetOpenIdAsync(It.IsAny())) .ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId }); // First request: Redis lock succeeds redisMock.SetupSequence(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true) // First request succeeds .ReturnsAsync(false); // Second request fails (debounce) // First login should succeed var firstResult = await authService.WechatMiniProgramLoginAsync("test_code_1", null, null); // Second login should be rejected due to debounce var secondResult = await authService.WechatMiniProgramLoginAsync("test_code_1", null, null); return firstResult.Success && !secondResult.Success && secondResult.ErrorMessage == "请勿频繁登录"; } /// /// Property 5: 推荐关系记录 /// For any new user registration with a valid pid (recommender ID), /// the system should correctly save the pid to the user record. /// Validates: Requirements 1.8, 2.7 /// Feature: user-auth-migration, Property 5: 推荐关系记录 /// [Property(MaxTest = 100)] public async Task RecommenderRelationshipIsRecorded(PositiveInt pid) { var dbContext = CreateInMemoryDbContext(); var (authService, wechatMock, redisMock) = CreateAuthService(dbContext); var openId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8); // Mock WeChat API wechatMock.Setup(x => x.GetOpenIdAsync(It.IsAny())) .ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId }); // Mock Redis to allow login redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act: Login with pid var result = await authService.WechatMiniProgramLoginAsync("test_code", pid.Item, null); // Assert: User should have the correct pid var user = await dbContext.Users.FirstOrDefaultAsync(u => u.OpenId == openId); return result.Success && user != null && user.Pid == pid.Item; } /// /// Property 6: 验证码验证与清理 /// For any mobile login request: /// - If verification code is correct, login should succeed and code should be deleted from Redis /// - If verification code is incorrect or expired, login should fail with "验证码错误" /// Validates: Requirements 2.1, 2.2 /// Feature: user-auth-migration, Property 6: 验证码验证与清理 /// [Property(MaxTest = 100)] public async Task VerificationCodeValidationAndCleanup() { var dbContext = CreateInMemoryDbContext(); var (authService, wechatMock, redisMock) = CreateAuthService(dbContext); var mobile = "138" + Random.Shared.Next(10000000, 99999999).ToString(); var correctCode = "123456"; var wrongCode = "654321"; // Mock Redis to allow login (no debounce) redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Test 1: Correct code should succeed redisMock.Setup(x => x.GetStringAsync($"sms:code:{mobile}")) .ReturnsAsync(correctCode); redisMock.Setup(x => x.DeleteAsync($"sms:code:{mobile}")) .Returns(Task.CompletedTask); var successResult = await authService.MobileLoginAsync(mobile, correctCode, null, null); // Verify DeleteAsync was called (code cleanup) redisMock.Verify(x => x.DeleteAsync($"sms:code:{mobile}"), Times.Once); // Test 2: Wrong code should fail var dbContext2 = CreateInMemoryDbContext(); var (authService2, _, redisMock2) = CreateAuthService(dbContext2); redisMock2.Setup(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); redisMock2.Setup(x => x.GetStringAsync($"sms:code:{mobile}")) .ReturnsAsync(correctCode); var failResult = await authService2.MobileLoginAsync(mobile, wrongCode, null, null); return successResult.Success && !failResult.Success && failResult.ErrorMessage == "验证码错误"; } /// /// Property 8: 数据库Token兼容存储 /// For any successful login request, the system should store account_token in UserAccount table /// to maintain compatibility with the legacy system. /// Validates: Requirements 3.6 /// Feature: user-auth-migration, Property 8: 数据库Token兼容存储 /// [Property(MaxTest = 100)] public async Task DatabaseTokenCompatibilityStorage() { var dbContext = CreateInMemoryDbContext(); var (authService, wechatMock, redisMock) = CreateAuthService(dbContext); var openId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8); // Mock WeChat API wechatMock.Setup(x => x.GetOpenIdAsync(It.IsAny())) .ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId }); // Mock Redis to allow login redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act var result = await authService.WechatMiniProgramLoginAsync("test_code", null, null); // Assert: UserAccount should be created with account_token var userAccount = await dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == result.UserId); return result.Success && userAccount != null && !string.IsNullOrWhiteSpace(userAccount.AccountToken) && !string.IsNullOrWhiteSpace(userAccount.TokenNum); } }