using FsCheck;
using FsCheck.Xunit;
using XiangYi.Application.Services;
namespace XiangYi.Application.Tests.Services;
///
/// AuthService属性测试
///
public class AuthServicePropertyTests
{
///
/// **Feature: backend-api, Property 1: 相亲编号格式正确性**
/// **Validates: Requirements 1.4**
///
/// *For any* 新创建的用户, 其相亲编号应为6位数字且首位不为0
///
[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;
});
}
///
/// 相亲编号首位范围测试 - 首位应在1-9之间
///
[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;
});
}
///
/// 相亲编号数值范围测试 - 应在100000-999999之间
///
[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;
});
}
}
///
/// JWT Token属性测试
///
public class JwtTokenPropertyTests
{
///
/// **Feature: backend-api, Property 23: JWT Token过期验证**
/// **Validates: Requirements 1.3**
///
/// *For any* 使用过期Token的请求, 应返回401状态码
///
/// 此测试验证:生成的Token包含正确的过期时间,且过期Token可被正确识别
///
[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;
});
}
///
/// 验证过期Token应该被正确识别为无效
///
[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; // 其他异常表示测试失败
}
});
}
///
/// 验证有效Token应该能被正确验证
///
[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;
}
});
}
}