132 lines
5.8 KiB
C#
132 lines
5.8 KiB
C#
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")));
|
||
}
|
||
}
|