309 lines
12 KiB
C#
309 lines
12 KiB
C#
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;
|
||
}
|
||
});
|
||
}
|
||
}
|