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,131 @@
using System.Linq;
using System.Text.Json;
using Xunit;
namespace Yh.Platform.AppAuth.Tests;
/// <summary>
/// AppSignHandler basePath 剥离对拍(验收 P1 修复):
/// 链路 handler(fullUrl) → 剥离 basePath → SignUtil(剥离后 canonicalTarget) → 向量基线(Java 口径)。
/// 证明:经 handler 发出的签名 = Java 客户端对同一 URL 的签名(同 ts 时逐字节一致)。
/// </summary>
public class AppSignHandlerTests
{
private sealed class CaptureHandler : HttpMessageHandler
{
public HttpRequestMessage? Last { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
Last = request;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
private const string AppKey = "100476";
private const string Secret = "app-secret-100476-yunhai";
private static async Task<HttpRequestMessage> SendThroughHandlerAsync(AppSignHandler handler, HttpMethod method, string url)
{
using var client = new HttpClient(handler);
var request = new HttpRequestMessage(method, url);
await client.SendAsync(request);
// handler 已把三头写入同一个 request 实例;CaptureHandler 捕获的是该实例
return request;
}
/// <summary>读取请求级向量:固定 ts 后,经 handler 对 fullUrl 的签名须与向量基线(Java 口径)逐字节一致。</summary>
public static IEnumerable<object[]> RequestLevelCases()
{
var path = Path.Combine(AppContext.BaseDirectory, "fixtures", "sign-vectors.json");
var root = JsonDocument.Parse(File.ReadAllText(path)).RootElement;
foreach (var c in root.GetProperty("cases").EnumerateArray())
{
if (c.TryGetProperty("fullUrl", out _))
{
yield return new object[] { c.Clone() };
}
}
}
[Theory]
[MemberData(nameof(RequestLevelCases))]
public async Task Handler对fullUrl签名_与Java基线逐字节一致(JsonElement c)
{
var name = c.GetProperty("name").GetString();
var method = new HttpMethod(c.GetProperty("method").GetString()!);
var fullUrl = c.GetProperty("fullUrl").GetString()!;
var basePath = c.GetProperty("basePath").GetString()!;
var ts = c.GetProperty("ts").GetString()!;
var secret = c.GetProperty("secret").GetString()!;
var expected = c.GetProperty("expectedRequestSignBase64").GetString()!;
// 固定 ts = 向量 ts(SignUtil.NowMillis 为 handler 与 SignUtil 共用的时钟源)
var original = SignUtil.NowMillis;
SignUtil.NowMillis = () => long.Parse(ts);
try
{
var handler = new AppSignHandler(AppKey, secret, basePath, new CaptureHandler());
var sent = await SendThroughHandlerAsync(handler, method, fullUrl);
Assert.Equal(AppKey, sent.Headers.GetValues(AppSignHandler.HeaderAppKey).First());
Assert.Equal(ts, sent.Headers.GetValues(AppSignHandler.HeaderTimestamp).First());
// 同 URL、同 ts 下与 Java 客户端(AppSignInterceptor 剥 basePath 后)签名逐字节一致
Assert.Equal(expected, sent.Headers.GetValues(AppSignHandler.HeaderSignature).First());
}
finally
{
SignUtil.NowMillis = original;
}
}
[Fact]
public async Task Handler剥离basePath_并排序query后签名()
{
var handler = new AppSignHandler(AppKey, Secret, "/userbase", new CaptureHandler());
var sent = await SendThroughHandlerAsync(handler, HttpMethod.Get,
"http://base.bestsch.com/userbase/oapi/user/byId?b=2&a=1");
var ts = sent.Headers.GetValues(AppSignHandler.HeaderTimestamp).First();
var signature = sent.Headers.GetValues(AppSignHandler.HeaderSignature).First();
// 剥离 /userbase + query 排序:canonical = /oapi/user/byId?a=1&b=2
Assert.Equal(SignUtil.SignRequest("GET", "/oapi/user/byId?a=1&b=2", ts, Secret), signature);
}
[Fact]
public async Task Handler未配置basePath_不剥离原样签名()
{
var handler = new AppSignHandler(AppKey, Secret, null, new CaptureHandler());
var sent = await SendThroughHandlerAsync(handler, HttpMethod.Get,
"http://base.bestsch.com/userbase/oapi/user/byId");
var ts = sent.Headers.GetValues(AppSignHandler.HeaderTimestamp).First();
var signature = sent.Headers.GetValues(AppSignHandler.HeaderSignature).First();
Assert.Equal(SignUtil.SignRequest("GET", "/userbase/oapi/user/byId", ts, Secret), signature);
}
[Theory]
[InlineData("/userbase/oapi/u", "/userbase", "/oapi/u")]
[InlineData("/userbase", "/userbase", "/")]
[InlineData("/userbase/", "/userbase", "/")]
[InlineData("/userbase/oapi/u", "/userbase/", "/oapi/u")] // basePath 带尾斜杠归一化
[InlineData("/userbase2/x", "/userbase", "/userbase2/x")] // 前缀不完整匹配不剥离(对齐 Java)
[InlineData("/oapi/u", "/userbase", "/oapi/u")] // 无前缀不剥离
[InlineData("/userbase/oapi/u", null, "/userbase/oapi/u")] // basePath 空 = 挂根路径
public void StripBasePath_边界与Java一致(string path, string? basePath, string expected)
{
Assert.Equal(expected, SignCanonical.StripBasePath(path, basePath));
}
[Fact]
public void BuildTarget_带basePath_剥离并排序query()
{
Assert.Equal("/oapi/user/byId?a=1&b=2",
SignCanonical.BuildTarget(new Uri("http://h/userbase/oapi/user/byId?b=2&a=1"), "/userbase"));
// 不传 basePath 保持既有行为(不剥离,query 仍排序)
Assert.Equal("/userbase/oapi/user/byId?a=1&b=2",
SignCanonical.BuildTarget(new Uri("http://h/userbase/oapi/user/byId?b=2&a=1")));
}
}