HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/AuthServiceLoginRecordPropertyTests.cs
2026-01-25 19:10:31 +08:00

242 lines
8.5 KiB
C#

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;
/// <summary>
/// AuthService登录记录和账号注销属性测试
/// 测试登录记录和账号注销相关的核心属性
/// </summary>
public class AuthServiceLoginRecordPropertyTests
{
private HoneyBoxDbContext CreateInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new HoneyBoxDbContext(options);
}
private (AuthService authService, Mock<IIpLocationService> ipLocationMock) CreateAuthService(HoneyBoxDbContext dbContext)
{
var mockUserLogger = new Mock<ILogger<UserService>>();
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<ILogger<JwtService>>();
var jwtService = new JwtService(jwtSettings, mockJwtLogger.Object);
var mockWechatService = new Mock<IWechatService>();
var mockIpLocationService = new Mock<IIpLocationService>();
var mockRedisService = new Mock<IRedisService>();
var mockAuthLogger = new Mock<ILogger<AuthService>>();
var authService = new AuthService(
dbContext,
userService,
jwtService,
mockWechatService.Object,
mockIpLocationService.Object,
mockRedisService.Object,
jwtSettings,
mockAuthLogger.Object);
return (authService, mockIpLocationService);
}
/// <summary>
/// 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: 登录日志记录
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> LoginLogRecording()
{
var dbContext = CreateInMemoryDbContext();
var (authService, ipLocationMock) = CreateAuthService(dbContext);
// Setup IP location mock
ipLocationMock.Setup(x => x.GetLocationAsync(It.IsAny<string>()))
.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;
}
/// <summary>
/// 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接口返回值
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> RecordLoginReturnValue()
{
var dbContext = CreateInMemoryDbContext();
var (authService, ipLocationMock) = CreateAuthService(dbContext);
ipLocationMock.Setup(x => x.GetLocationAsync(It.IsAny<string>()))
.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;
}
/// <summary>
/// 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: 账号注销类型处理
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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;
}
}