HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/AuthServicePropertyTests.cs
2026-01-04 01:47:02 +08:00

267 lines
11 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 AuthServicePropertyTests
{
private HoneyBoxDbContext CreateInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new HoneyBoxDbContext(options);
}
private (AuthService authService, Mock<IWechatService> wechatMock, Mock<IRedisService> redisMock) 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>>();
// Default IP location result
mockIpLocationService.Setup(x => x.GetLocationAsync(It.IsAny<string>()))
.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);
}
/// <summary>
/// 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: 微信登录用户查找优先级
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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<string>()))
.ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId, UnionId = unionId });
// Mock Redis to allow login (no debounce)
redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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;
}
/// <summary>
/// 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: 登录防抖机制
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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<string>()))
.ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId });
// First request: Redis lock succeeds
redisMock.SetupSequence(x => x.TryAcquireLockAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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 == "请勿频繁登录";
}
/// <summary>
/// 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: 推荐关系记录
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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<string>()))
.ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId });
// Mock Redis to allow login
redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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;
}
/// <summary>
/// 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: 验证码验证与清理
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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 == "验证码错误";
}
/// <summary>
/// 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兼容存储
/// </summary>
[Property(MaxTest = 100)]
public async Task<bool> 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<string>()))
.ReturnsAsync(new WechatAuthResult { Success = true, OpenId = openId });
// Mock Redis to allow login
redisMock.Setup(x => x.TryAcquireLockAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>()))
.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);
}
}