Files
yunhai.netcore.tool/BasePlatformDotnet/tests/Yh.Platform.AppAuth.Tests/AppSignHandlerTests.cs
T
2026-09-02 09:38:45 +08:00

132 lines
5.8 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.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")));
}
}