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; } }); } }