campus-errand/server.tests/JwtAuthPropertyTests.cs
2026-03-01 05:01:47 +08:00

140 lines
5.3 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 System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using FsCheck;
using FsCheck.Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.IdentityModel.Tokens;
namespace CampusErrand.Tests;
/// <summary>
/// Property 26: JWT 认证
/// 验证:未携带有效 JWT 时返回 401携带有效 JWT 但非管理员访问管理接口时返回 403。
/// **Feature: login-and-homepage, Property 26: JWT 认证**
/// **Validates: Requirements 32.1, 32.3, 32.4, 32.5**
/// </summary>
public class JwtAuthPropertyTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
// 与 appsettings.json 中一致的配置
private const string Secret = "YourSuperSecretKeyForJwtTokenGeneration_AtLeast32Chars!";
private const string Issuer = "CampusErrand";
private const string Audience = "CampusErrandApp";
public JwtAuthPropertyTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
/// <summary>
/// 生成有效的 JWT 令牌
/// </summary>
private static string GenerateValidToken(int userId, string role, int expireMinutes = 60)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Role, role)
};
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(expireMinutes),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 生成已过期的 JWT 令牌
/// </summary>
private static string GenerateExpiredToken(int userId, string role)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Role, role)
};
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(-1),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 属性:对任意用户 ID未携带 JWT 访问受保护接口应返回 401
/// </summary>
[Property(MaxTest = 20)]
public bool Token访问受保护接口返回401(PositiveInt userId)
{
var client = _factory.CreateClient();
var response = client.GetAsync("/api/protected").Result;
return response.StatusCode == HttpStatusCode.Unauthorized;
}
/// <summary>
/// 属性:对任意用户 ID携带有效 JWT 访问受保护接口应返回 200
/// </summary>
[Property(MaxTest = 20)]
public bool Token访问受保护接口返回200(PositiveInt userId)
{
var token = GenerateValidToken(userId.Get, "User");
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync("/api/protected").Result;
return response.StatusCode == HttpStatusCode.OK;
}
/// <summary>
/// 属性:对任意用户 ID非管理员角色访问管理接口应返回 403
/// </summary>
[Property(MaxTest = 20)]
public bool 访403(PositiveInt userId)
{
var token = GenerateValidToken(userId.Get, "User");
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync("/api/admin/protected").Result;
return response.StatusCode == HttpStatusCode.Forbidden;
}
/// <summary>
/// 属性:对任意用户 ID管理员角色访问管理接口应返回 200
/// </summary>
[Property(MaxTest = 20)]
public bool 访200(PositiveInt userId)
{
var token = GenerateValidToken(userId.Get, "Admin");
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync("/api/admin/protected").Result;
return response.StatusCode == HttpStatusCode.OK;
}
/// <summary>
/// 属性:对任意用户 ID过期 JWT 访问受保护接口应返回 401
/// </summary>
[Property(MaxTest = 20)]
public bool Token访问受保护接口返回401(PositiveInt userId)
{
var tokenStr = GenerateExpiredToken(userId.Get, "User");
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenStr);
var response = client.GetAsync("/api/protected").Result;
return response.StatusCode == HttpStatusCode.Unauthorized;
}
}