This commit is contained in:
wanghu
2026-09-02 09:38:45 +08:00
commit 8774b6cc44
128 changed files with 9382 additions and 0 deletions
@@ -0,0 +1,161 @@
using System.Security.Claims;
using Yh.Platform.Abstractions;
using Yh.Platform.Auth;
using Xunit;
namespace Yh.Platform.Auth.Tests;
/// <summary>CAS serviceValidate 响应 XML 用户提取对拍(对齐 Java CasServiceTicketValidator)。</summary>
public class CasServiceTicketValidatorTests
{
[Fact]
public void 成功响应_提取cas_user()
{
var xml = """
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationSuccess>
<cas:user>zhangsan</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
""";
Assert.Equal("zhangsan", CasServiceTicketValidator.ExtractUser(xml));
}
[Fact]
public void 带属性容错_提取cas_user()
{
var xml = "<cas:serviceResponse><cas:authenticationSuccess><cas:user index=\"0\">lisi</cas:user></cas:authenticationSuccess></cas:serviceResponse>";
Assert.Equal("lisi", CasServiceTicketValidator.ExtractUser(xml));
}
[Fact]
public void 失败响应_返回null()
{
var xml = """
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationFailure code="INVALID_TICKET">
Ticket ST-1856339 not recognized
</cas:authenticationFailure>
</cas:serviceResponse>
""";
Assert.Null(CasServiceTicketValidator.ExtractUser(xml));
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("<cas:serviceResponse></cas:serviceResponse>")]
public void 无用户节点或空输入_返回null(string? xml)
{
Assert.Null(CasServiceTicketValidator.ExtractUser(xml));
}
[Fact]
public async Task 未配置CAS地址_抛AuthException()
{
var validator = new CasServiceTicketValidator("");
var ex = await Assert.ThrowsAsync<AuthException>(() => validator.ValidateAsync("ST-1", "http://app/cb"));
Assert.Contains("cas-server-url-prefix", ex.Message);
}
}
public class BoaUserContextTests
{
[Fact]
public async Task 异步流内可读取_请求结束清理()
{
Assert.Null(BoaUserContext.Get());
Assert.Null(UserContextHolder.Get());
BoaUser? seenInChild = null;
var user = new BoaUser(1001, "zhangsan", "张三");
BoaUserContext.Set(user);
UserContextHolder.Set(new LoginUser("1001") { LoginName = "zhangsan", DisplayName = "张三" });
await Task.Run(() =>
{
// AsyncLocal 值随执行上下文流入子任务
seenInChild = BoaUserContext.Get();
});
Assert.Same(user, seenInChild);
Assert.Equal("zhangsan", UserContextHolder.Get()?.LoginName);
BoaUserContext.Clear();
UserContextHolder.Clear();
Assert.Null(BoaUserContext.Get());
Assert.Null(UserContextHolder.Get());
}
[Fact]
public void LoginUser_属性封装()
{
var login = new LoginUser("1001")
{
LoginName = "zhangsan",
DisplayName = "张三",
AppKey = "app-001",
};
Assert.Equal("1001", login.UserId);
Assert.Equal("zhangsan", login.LoginName);
Assert.Equal("张三", login.DisplayName);
Assert.Equal("app-001", login.AppKey);
}
}
public class UserContextMiddlewareTests
{
private sealed class StubProvider : IUserDetailsProvider
{
public BoaUser? User { get; set; } = new(1001, "zhangsan", "张三");
public Task<BoaUser?> GetByLoginNameAsync(string loginName, CancellationToken ct = default)
=> Task.FromResult(User);
}
private static HttpContext HttpContextWithLogin(string loginName)
{
var identity = new ClaimsIdentity("CAS");
identity.AddClaim(new Claim("loginName", loginName));
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(identity);
return context;
}
[Fact]
public async Task 认证请求_中间件写入上下文_业务管道内可读()
{
var provider = new StubProvider();
BoaUser? seen = null;
LoginUser? seenLogin = null;
var middleware = new UserContextMiddleware(_ =>
{
seen = BoaUserContext.Get();
seenLogin = UserContextHolder.Get();
return Task.CompletedTask;
});
await middleware.InvokeAsync(HttpContextWithLogin("zhangsan"), provider);
Assert.NotNull(seen);
Assert.Equal(1001, seen!.Id);
Assert.Equal("zhangsan", seenLogin?.LoginName);
Assert.Equal("张三", seenLogin?.DisplayName);
// 请求结束后必须清空,防串扰
Assert.Null(BoaUserContext.Get());
Assert.Null(UserContextHolder.Get());
}
[Fact]
public async Task 未认证请求_不写上下文直接放行()
{
var provider = new StubProvider();
var middleware = new UserContextMiddleware(_ => Task.CompletedTask);
var context = new DefaultHttpContext(); // 匿名
await middleware.InvokeAsync(context, provider);
Assert.Null(BoaUserContext.Get());
Assert.Null(UserContextHolder.Get());
}
}
@@ -0,0 +1,8 @@
global using System;
global using System.Collections.Generic;
global using System.Security.Claims;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Http;
global using Yh.Platform.Abstractions;
global using Yh.Platform.Auth;
@@ -0,0 +1,104 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Yh.Platform.Auth;
using Xunit;
namespace Yh.Platform.Auth.Tests;
/// <summary>
/// 会话票据格式(SessionTicketDataFormat)单测:
/// 32 位 key 往返、未知 key / 过期票据失效、显式退出(Revoke)立即吊销。
/// </summary>
public class SessionTicketDataFormatTests
{
private static AuthenticationTicket NewTicket(DateTimeOffset? expiresUtc = null)
{
var identity = new ClaimsIdentity("CAS");
identity.AddClaim(new Claim(ClaimTypes.Name, "zhangsan"));
var principal = new ClaimsPrincipal(identity);
var props = new AuthenticationProperties();
if (expiresUtc.HasValue)
{
props.ExpiresUtc = expiresUtc;
}
return new AuthenticationTicket(principal, props, "YhCookie");
}
[Fact]
public void Protect_返回32位key_且可往返()
{
var format = new SessionTicketDataFormat();
var key = format.Protect(NewTicket());
Assert.Equal(32, key.Length); // Guid N 格式 = 32 个十六进制字符
var ticket = format.Unprotect(key);
Assert.NotNull(ticket);
Assert.Equal("zhangsan", ticket!.Principal.FindFirstValue(ClaimTypes.Name));
}
[Fact]
public void Unprotect_未知key_返回null()
{
var format = new SessionTicketDataFormat();
Assert.Null((AuthenticationTicket?)format.Unprotect("00000000000000000000000000000000"));
}
[Fact]
public void Unprotect_空或null_返回null()
{
var format = new SessionTicketDataFormat();
Assert.Null((AuthenticationTicket?)format.Unprotect(null));
Assert.Null((AuthenticationTicket?)format.Unprotect(""));
}
[Fact]
public void Revoke_票据立即失效()
{
var format = new SessionTicketDataFormat();
var key = format.Protect(NewTicket());
format.Revoke(key);
Assert.Null((AuthenticationTicket?)format.Unprotect(key));
}
[Fact]
public void Revoke_空key_不抛异常()
{
var format = new SessionTicketDataFormat();
format.Revoke(null);
format.Revoke("");
format.Revoke(" ");
}
[Fact]
public void Unprotect_过期票据_返回null()
{
var format = new SessionTicketDataFormat();
var key = format.Protect(NewTicket(DateTimeOffset.UtcNow.AddSeconds(-1)));
Assert.Null((AuthenticationTicket?)format.Unprotect(key));
}
[Fact]
public void Unprotect_未过期票据_正常往返()
{
var format = new SessionTicketDataFormat();
var key = format.Protect(NewTicket(DateTimeOffset.UtcNow.AddMinutes(1)));
Assert.NotNull(format.Unprotect(key));
}
[Fact]
public void Revoke_不影响其他票据()
{
var format = new SessionTicketDataFormat();
var keyA = format.Protect(NewTicket());
var keyB = format.Protect(NewTicket());
format.Revoke(keyA);
Assert.Null((AuthenticationTicket?)format.Unprotect(keyA));
Assert.NotNull(format.Unprotect(keyB));
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Yh.Platform.Auth\Yh.Platform.Auth.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>