bag
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user