test: share one operator and trust root across the trust tests; make the setup test cleanup best-effort [CI SKIP]
This commit is contained in:
1 parent
f22fdf8b68
commit
4a1e48db20
4 files changed
+91
-87
No files matched your search
@@ -0,0 +1,70 @@
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Fleet.Identity;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>一个已经建立了信任根的操作员:自己的用户、passkey、会话,以及创世指纹。</summary>
|
||||
internal sealed record Operator(HttpClient Session, SoftwarePasskey Passkey, string Genesis);
|
||||
|
||||
/// <summary>
|
||||
/// 共享的后端上只有一个信任根(创世只能签一次):谁先需要,谁来建立,其余的测试用同一个操作员。
|
||||
/// 信任相关的测试都从这里拿,不要各自去建立信任根,否则测试的先后顺序会决定谁失败。
|
||||
/// </summary>
|
||||
internal static class SharedOperator
|
||||
{
|
||||
private static readonly ConditionalWeakTable<BackendFixture, Operator> s_operators = [];
|
||||
private static readonly SemaphoreSlim s_gate = new(1, 1);
|
||||
|
||||
public static async Task<Operator> GetAsync(BackendFixture backend)
|
||||
{
|
||||
await s_gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (s_operators.TryGetValue(backend, out Operator? existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 操作员:有 passkey、用 passkey 登录的会话
|
||||
(HttpClient session, SoftwarePasskey passkey, _) = await backend.CreateStrongSessionAsync("operator");
|
||||
|
||||
// 创世:用 passkey 对集合本身签名(节点据此验链)
|
||||
JsonNode state = JsonNode.Parse(await session.GetStringAsync("/api/admin/trust"))!;
|
||||
Assert.False(state["initialized"]!.GetValue<bool>());
|
||||
string statement = state["proposal"]!["statement"]!.GetValue<string>();
|
||||
Assert.Equal(Base64UrlOf(statement), state["proposal"]!["challenge"]!.GetValue<string>());
|
||||
using HttpResponseMessage genesis = await session.PostAsync("/api/admin/trust", BackendFixture.Json(Sign(passkey, statement)));
|
||||
Assert.True(genesis.StatusCode == HttpStatusCode.Created, await genesis.Content.ReadAsStringAsync());
|
||||
string fingerprint = JsonNode.Parse(await genesis.Content.ReadAsStringAsync())!["genesis"]!.GetValue<string>();
|
||||
Assert.Equal(OperatorTrust.Fingerprint(statement), fingerprint);
|
||||
|
||||
var made = new Operator(session, passkey, fingerprint);
|
||||
s_operators.Add(backend, made);
|
||||
return made;
|
||||
}
|
||||
finally
|
||||
{
|
||||
s_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static string Base64UrlOf(string statement) => Fleet.Core.Base64Url.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(statement)));
|
||||
|
||||
/// <summary>passkey 对语句签名(挑战是语句的 SHA-256),拼成接口要的断言对象。</summary>
|
||||
public static JsonObject Sign(SoftwarePasskey passkey, string statement)
|
||||
{
|
||||
JsonObject assertion = passkey.Assert(Base64UrlOf(statement));
|
||||
return new JsonObject
|
||||
{
|
||||
["statement"] = statement,
|
||||
["credential_id"] = assertion["credential_id"]!.DeepClone(),
|
||||
["authenticator_data"] = assertion["authenticator_data"]!.DeepClone(),
|
||||
["client_data_json"] = assertion["client_data_json"]!.DeepClone(),
|
||||
["signature"] = assertion["signature"]!.DeepClone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user