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 AuthServiceLoginRecordPropertyTests { private HoneyBoxDbContext CreateInMemoryDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } private (AuthService authService, Mock ipLocationMock) 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>(); var authService = new AuthService( dbContext, userService, jwtService, mockWechatService.Object, mockIpLocationService.Object, mockRedisService.Object, jwtSettings, mockAuthLogger.Object); return (authService, mockIpLocationService); } /// /// Property 15: 登录日志记录 /// For any successful login, the system should: /// - Create a record in UserLoginLog table /// - Update UserAccount table with last login time and IP info /// Validates: Requirements 6.1, 6.3 /// Feature: user-auth-migration, Property 15: 登录日志记录 /// [Property(MaxTest = 100)] public async Task LoginLogRecording() { var dbContext = CreateInMemoryDbContext(); var (authService, ipLocationMock) = CreateAuthService(dbContext); // Setup IP location mock ipLocationMock.Setup(x => x.GetLocationAsync(It.IsAny())) .ReturnsAsync(new IpLocationResult { Success = true, Province = "北京", City = "北京", Adcode = "110000" }); // Create a user var user = new User { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Uid = "uid123", Nickname = "TestUser", HeadImg = "https://example.com/avatar.jpg", Status = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await dbContext.Users.AddAsync(user); // Create UserAccount for the user var userAccount = new UserAccount { UserId = user.Id, AccountToken = "token123", TokenNum = "num123", LastLoginIp = string.Empty }; await dbContext.UserAccounts.AddAsync(userAccount); await dbContext.SaveChangesAsync(); var device = "iOS"; var clientIp = "192.168.1.1"; // Act await authService.RecordLoginAsync(user.Id, device, clientIp); // Assert // 1. UserLoginLog should be created var loginLog = await dbContext.UserLoginLogs.FirstOrDefaultAsync(l => l.UserId == user.Id); var logCreated = loginLog != null && loginLog.Device == device; // 2. UserAccount should be updated var updatedAccount = await dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == user.Id); var accountUpdated = updatedAccount != null && updatedAccount.LastLoginTime != null && updatedAccount.IpProvince == "北京"; return logCreated && accountUpdated; } /// /// Property 16: recordLogin接口返回值 /// For any recordLogin call, the system should return: /// - User's uid /// - User's nickname /// - User's headimg /// Validates: Requirements 6.4 /// Feature: user-auth-migration, Property 16: recordLogin接口返回值 /// [Property(MaxTest = 100)] public async Task RecordLoginReturnValue() { var dbContext = CreateInMemoryDbContext(); var (authService, ipLocationMock) = CreateAuthService(dbContext); ipLocationMock.Setup(x => x.GetLocationAsync(It.IsAny())) .ReturnsAsync(new IpLocationResult { Success = true }); var expectedUid = "uid_" + Random.Shared.Next(1000, 9999); var expectedNickname = "TestUser_" + Random.Shared.Next(1000, 9999); var expectedHeadimg = "https://example.com/avatar_" + Random.Shared.Next(1000, 9999) + ".jpg"; // Create a user var user = new User { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Uid = expectedUid, Nickname = expectedNickname, HeadImg = expectedHeadimg, Status = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await dbContext.Users.AddAsync(user); await dbContext.SaveChangesAsync(); // Act var result = await authService.RecordLoginAsync(user.Id, "Android", "192.168.1.1"); // Assert return result.Uid == expectedUid && result.Nickname == expectedNickname && result.Headimg == expectedHeadimg; } /// /// Property 17: 账号注销类型处理 /// For any log off request: /// - type=0 should deactivate the account (status=0) /// - type=1 should reactivate the account (status=1) /// Validates: Requirements 7.1, 7.2, 7.3 /// Feature: user-auth-migration, Property 17: 账号注销类型处理 /// [Property(MaxTest = 100)] public async Task LogOffTypeHandling() { // Test type=0 (deactivate) var dbContext1 = CreateInMemoryDbContext(); var (authService1, _) = CreateAuthService(dbContext1); var user1 = new User { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Uid = "uid123", Nickname = "TestUser", HeadImg = "https://example.com/avatar.jpg", Status = 1, // Active CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await dbContext1.Users.AddAsync(user1); await dbContext1.SaveChangesAsync(); // Act: Deactivate account await authService1.LogOffAsync(user1.Id, 0); // Assert: Status should be 0 var deactivatedUser = await dbContext1.Users.FirstOrDefaultAsync(u => u.Id == user1.Id); var deactivateSuccess = deactivatedUser?.Status == 0; // Test type=1 (reactivate) var dbContext2 = CreateInMemoryDbContext(); var (authService2, _) = CreateAuthService(dbContext2); var user2 = new User { OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8), Uid = "uid456", Nickname = "TestUser2", HeadImg = "https://example.com/avatar.jpg", Status = 0, // Inactive CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await dbContext2.Users.AddAsync(user2); await dbContext2.SaveChangesAsync(); // Act: Reactivate account await authService2.LogOffAsync(user2.Id, 1); // Assert: Status should be 1 var reactivatedUser = await dbContext2.Users.FirstOrDefaultAsync(u => u.Id == user2.Id); var reactivateSuccess = reactivatedUser?.Status == 1; return deactivateSuccess && reactivateSuccess; } }