xiangyixiangqin/server/tests/XiangYi.Application.Tests/Services/AuthServicePropertyTests.cs
2026-01-02 18:00:49 +08:00

309 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FsCheck;
using FsCheck.Xunit;
using XiangYi.Application.Services;
namespace XiangYi.Application.Tests.Services;
/// <summary>
/// AuthService属性测试
/// </summary>
public class AuthServicePropertyTests
{
/// <summary>
/// **Feature: backend-api, Property 1: 相亲编号格式正确性**
/// **Validates: Requirements 1.4**
///
/// *For any* 新创建的用户, 其相亲编号应为6位数字且首位不为0
/// </summary>
[Property(MaxTest = 100)]
public Property XiangQinNo_ShouldBe6DigitsAndNotStartWithZero()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
_ =>
{
// Arrange & Act
var xiangQinNo = AuthService.GenerateXiangQinNo();
// Assert
// 1. 长度必须为6位
var isLength6 = xiangQinNo.Length == 6;
// 2. 首位不能为0
var firstDigitNotZero = xiangQinNo[0] != '0';
// 3. 所有字符必须是数字
var allDigits = xiangQinNo.All(char.IsDigit);
return isLength6 && firstDigitNotZero && allDigits;
});
}
/// <summary>
/// 相亲编号首位范围测试 - 首位应在1-9之间
/// </summary>
[Property(MaxTest = 100)]
public Property XiangQinNo_FirstDigitShouldBeBetween1And9()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
_ =>
{
var xiangQinNo = AuthService.GenerateXiangQinNo();
var firstDigit = int.Parse(xiangQinNo[0].ToString());
return firstDigit >= 1 && firstDigit <= 9;
});
}
/// <summary>
/// 相亲编号数值范围测试 - 应在100000-999999之间
/// </summary>
[Property(MaxTest = 100)]
public Property XiangQinNo_ValueShouldBeInValidRange()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
_ =>
{
var xiangQinNo = AuthService.GenerateXiangQinNo();
var value = int.Parse(xiangQinNo);
// 6位数字首位不为0范围应该是100000-999999
return value >= 100000 && value <= 999999;
});
}
}
/// <summary>
/// JWT Token属性测试
/// </summary>
public class JwtTokenPropertyTests
{
/// <summary>
/// **Feature: backend-api, Property 23: JWT Token过期验证**
/// **Validates: Requirements 1.3**
///
/// *For any* 使用过期Token的请求, 应返回401状态码
///
/// 此测试验证生成的Token包含正确的过期时间且过期Token可被正确识别
/// </summary>
[Property(MaxTest = 100)]
public Property GeneratedToken_ShouldContainValidExpirationClaim()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
positiveInt =>
{
var userId = positiveInt.Get;
// Arrange
var jwtOptions = new XiangYi.Application.Options.JwtOptions
{
Secret = "test-secret-key-that-is-at-least-256-bits-long-for-hmac-sha256",
Issuer = "TestIssuer",
Audience = "TestAudience",
ExpireMinutes = 60
};
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(jwtOptions.Secret));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new System.Security.Claims.Claim(
System.Security.Claims.ClaimTypes.NameIdentifier, userId.ToString()),
new System.Security.Claims.Claim("openid", $"test_openid_{userId}"),
new System.Security.Claims.Claim(
System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString())
};
var expireTime = DateTime.UtcNow.AddMinutes(jwtOptions.ExpireMinutes);
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: jwtOptions.Issuer,
audience: jwtOptions.Audience,
claims: claims,
expires: expireTime,
signingCredentials: credentials
);
var tokenString = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler()
.WriteToken(token);
// Act - 解析Token
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var parsedToken = handler.ReadJwtToken(tokenString);
// Assert
// 1. Token应该包含过期时间
var hasExpClaim = parsedToken.Payload.Expiration.HasValue;
// 2. 过期时间应该在未来(当前时间之后)
var expTime = DateTimeOffset.FromUnixTimeSeconds(parsedToken.Payload.Expiration!.Value).UtcDateTime;
var isInFuture = expTime > DateTime.UtcNow;
// 3. 过期时间应该在配置的时间范围内允许1分钟误差
var expectedExpTime = DateTime.UtcNow.AddMinutes(jwtOptions.ExpireMinutes);
var isWithinRange = Math.Abs((expTime - expectedExpTime).TotalMinutes) < 1;
return hasExpClaim && isInFuture && isWithinRange;
});
}
/// <summary>
/// 验证过期Token应该被正确识别为无效
/// </summary>
[Property(MaxTest = 100)]
public Property ExpiredToken_ShouldBeIdentifiedAsInvalid()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
positiveInt =>
{
var userId = positiveInt.Get;
// Arrange - 创建一个已过期的Token
var jwtOptions = new XiangYi.Application.Options.JwtOptions
{
Secret = "test-secret-key-that-is-at-least-256-bits-long-for-hmac-sha256",
Issuer = "TestIssuer",
Audience = "TestAudience",
ExpireMinutes = -1 // 负数表示已过期
};
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(jwtOptions.Secret));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new System.Security.Claims.Claim(
System.Security.Claims.ClaimTypes.NameIdentifier, userId.ToString()),
new System.Security.Claims.Claim("openid", $"test_openid_{userId}")
};
// 创建一个过期时间在过去的Token
var expireTime = DateTime.UtcNow.AddMinutes(-10); // 10分钟前过期
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: jwtOptions.Issuer,
audience: jwtOptions.Audience,
claims: claims,
expires: expireTime,
signingCredentials: credentials
);
var tokenString = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler()
.WriteToken(token);
// Act - 尝试验证Token
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var validationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true, // 验证过期时间
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = key,
ClockSkew = TimeSpan.Zero // 不允许时钟偏差
};
// Assert - 验证应该失败
try
{
handler.ValidateToken(tokenString, validationParameters, out _);
return false; // 如果没有抛出异常,测试失败
}
catch (Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException)
{
return true; // 正确抛出过期异常
}
catch
{
return false; // 其他异常表示测试失败
}
});
}
/// <summary>
/// 验证有效Token应该能被正确验证
/// </summary>
[Property(MaxTest = 100)]
public Property ValidToken_ShouldPassValidation()
{
return Prop.ForAll(
Arb.Default.PositiveInt(),
positiveInt =>
{
var userId = positiveInt.Get;
// Arrange - 创建一个有效的Token
var jwtOptions = new XiangYi.Application.Options.JwtOptions
{
Secret = "test-secret-key-that-is-at-least-256-bits-long-for-hmac-sha256",
Issuer = "TestIssuer",
Audience = "TestAudience",
ExpireMinutes = 60
};
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(jwtOptions.Secret));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new System.Security.Claims.Claim(
System.Security.Claims.ClaimTypes.NameIdentifier, userId.ToString()),
new System.Security.Claims.Claim("openid", $"test_openid_{userId}")
};
var expireTime = DateTime.UtcNow.AddMinutes(jwtOptions.ExpireMinutes);
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: jwtOptions.Issuer,
audience: jwtOptions.Audience,
claims: claims,
expires: expireTime,
signingCredentials: credentials
);
var tokenString = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler()
.WriteToken(token);
// Act - 验证Token
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var validationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = key,
ClockSkew = TimeSpan.FromMinutes(1)
};
// Assert - 验证应该成功
try
{
var principal = handler.ValidateToken(tokenString, validationParameters, out var validatedToken);
// 验证用户ID是否正确
var userIdClaim = principal.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
return userIdClaim != null && userIdClaim.Value == userId.ToString();
}
catch
{
return false;
}
});
}
}