using System.Text.Json; using System.Threading.Channels; using Fleet.Core; using Fleet.Rpc; namespace Fleet.Tests; /// 协议方向声明:路由器是唯一卡口,注册、分发、发送三处都核对声明表。 public sealed class MethodDirectionTests { private static readonly MethodTable s_table = TestTables.Of( MethodSpec.Request("t.ping", RpcSide.Node), MethodSpec.Notification("t.note", RpcSide.Node, "agent"), MethodSpec.Request("t.push", RpcSide.Server), MethodSpec.Request("t.inline", RpcSide.Node).AsInline(), // 同一个方法名在对等面里另有发起方:按(平面,名字)区分 MethodSpec.Request("t.ping", RpcSide.Listener)); private static readonly JsonElement s_empty = JsonDocument.Parse("{}").RootElement.Clone(); private static RpcMessage Request(string method) => new() { Id = 1, Method = method, Params = s_empty }; private static RpcMessage Notification(string method) => new() { Method = method, Params = s_empty }; private static RpcRouter Server(string? localRole = null) => new(RpcSide.Server, s_table, localRole, callerRole: role => role); [Fact] public void Registration_RefusesUndeclaredMethods_WrongDirection_WrongKind_AndWrongRole() { RpcRouter server = Server(); Assert.Throws(() => server.On("t.unknown", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => throw new NotSupportedException())); // t.push 是服务端发给节点的:服务端这一侧不能处理它 Assert.Throws(() => server.On("t.push", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => throw new NotSupportedException())); // t.ping 声明成请求:不能按通知注册 Assert.Throws(() => server.OnNotify("t.ping", EchoContext.Default.AddParams, (_, _, _) => ValueTask.CompletedTask)); // 节点这一侧:t.note 只给 agent 角色,probe 注册不了;t.push 是它该处理的 var probe = new RpcRouter(RpcSide.Node, s_table, "probe"); probe.On("t.push", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => throw new NotSupportedException()); Assert.Throws(() => new RpcRouter(RpcSide.Server, s_table, "probe").OnNotify("t.note", EchoContext.Default.AddParams, (_, _, _) => ValueTask.CompletedTask)); server.On("t.ping", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => ValueTask.FromResult(Result.Ok(new AddResult()))); } [Fact] public async Task Dispatch_RefusesWhatThePeerMayNotInitiate_AndReportsAViolation() { int calls = 0; RpcRouter server = Server() .On("t.ping", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => { calls++; return ValueTask.FromResult(Result.Ok(new AddResult())); }) .OnNotify("t.note", EchoContext.Default.AddParams, (_, _, _) => { calls++; return ValueTask.CompletedTask; }); // 正常 RpcDispatch ok = await server.DispatchCheckedAsync("agent", Request("t.ping"), default); Assert.Null(ok.Violation); Assert.NotNull(ok.Response!.Result); // 没有声明的方法 RpcDispatch undeclared = await server.DispatchCheckedAsync("agent", Request("t.nope"), default); Assert.NotNull(undeclared.Violation); Assert.Equal("fleet_protocol_violation", undeclared.Response!.Error!.Data!.Code); // 节点冒充服务端:t.push 只能由服务端发起 RpcDispatch backwards = await server.DispatchCheckedAsync("agent", Request("t.push"), default); Assert.NotNull(backwards.Violation); Assert.Equal("fleet_protocol_violation", backwards.Response!.Error!.Data!.Code); // 请求和通知用反了 Assert.NotNull((await server.DispatchCheckedAsync("agent", Notification("t.ping"), default)).Violation); RpcDispatch asRequest = await server.DispatchCheckedAsync("agent", Request("t.note"), default); Assert.NotNull(asRequest.Violation); Assert.NotNull(asRequest.Response); // 违规的消息到不了处理函数;通知违规不回响应 Assert.Equal(1, calls); RpcDispatch silentViolation = await server.DispatchCheckedAsync("agent", Notification("t.nope"), default); Assert.NotNull(silentViolation.Violation); Assert.Null(silentViolation.Response); } [Fact] public async Task Dispatch_ADeclaredMethodWithoutAHandler_IsUnsupported_NotAViolation() { RpcDispatch outcome = await Server().DispatchCheckedAsync("agent", Request("t.ping"), default); Assert.Null(outcome.Violation); Assert.Equal("fleet_unsupported", outcome.Response!.Error!.Data!.Code); } [Fact] public async Task Dispatch_ChecksTheCallersRole() { RpcRouter server = Server().OnNotify("t.note", EchoContext.Default.AddParams, (_, _, _) => ValueTask.CompletedTask); Assert.Null((await server.DispatchCheckedAsync("agent", Notification("t.note"), default)).Violation); Assert.NotNull((await server.DispatchCheckedAsync("probe", Notification("t.note"), default)).Violation); Assert.NotNull((await server.DispatchCheckedAsync(null!, Notification("t.note"), default)).Violation); } [Fact] public void CheckSend_RefusesWhatThisSideMayNotInitiate() { RpcRouter server = Server(); server.CheckSend("t.push", MethodKind.Request); Assert.Throws(() => server.CheckSend("t.ping", MethodKind.Request)); // 那是节点发起的 Assert.Throws(() => server.CheckSend("t.push", MethodKind.Notification)); // 类型不对 Assert.Throws(() => server.CheckSend("t.unknown", MethodKind.Request)); // 对端的角色也要允许 var node = new RpcRouter(RpcSide.Node, s_table, "agent"); node.CheckSend("t.note", MethodKind.Notification, "agent"); Assert.Throws(() => node.CheckSend("t.note", MethodKind.Notification, "probe")); } [Fact] public void MethodTable_RefusesDuplicates_AndReportsMissingHandlers() { Assert.Throws(() => TestTables.Of(TestTables.Req("a"), TestTables.Note("a"))); Assert.Throws(() => new MethodTable([], [new ChannelSpec(16, RpcSide.Node), new ChannelSpec(16, RpcSide.Server)])); // 服务端要处理 t.ping 和 t.note;t.inline 在握手阶段直接处理,不算缺 Assert.Equal(["t.note", "t.ping"], s_table.MissingHandlers(RpcSide.Server, null, [])); Assert.Equal(["t.ping"], s_table.MissingHandlers(RpcSide.Server, "probe", [])); Assert.Equal(["t.note"], s_table.MissingHandlers(RpcSide.Server, "agent", ["t.ping"])); Assert.Empty(s_table.MissingHandlers(RpcSide.Server, "agent", ["t.ping", "t.note"])); // 同名方法在对等面的接收方是拨号方?不:t.ping 在对等面由监听方发起,拨号方处理 Assert.Equal(["t.ping"], s_table.MissingHandlers(RpcSide.Dialer, null, [])); } [Fact] public async Task Session_ClosesTheConnection_AfterTooManyViolations() { (ChannelTransport a, ChannelTransport b) = ChannelTransport.Pair(); RpcRouter server = Server().On("t.ping", EchoContext.Default.AddParams, EchoContext.Default.AddResult, (_, _, _) => ValueTask.FromResult(Result.Ok(new AddResult()))); await using var session = new RpcSession(b, server, "agent", TimeProvider.System, new RpcSessionOptions { MaxViolations = 3 }); Task run = session.RunAsync(default); for (int i = 1; i <= 6; i++) { await a.SendAsync(new RpcFrame(FrameKind.Text, JsonSerializer.SerializeToUtf8Bytes(new RpcMessage { Id = i, Method = "t.nope", Params = s_empty }, FleetRpcContext.Default.RpcMessage)), default); } // 违规的请求先收到失败响应,超过上限之后连接被关闭 int failures = 0; while (await a.ReceiveAsync(default).AsTask().WaitAsync(TimeSpan.FromSeconds(5)) is { } frame) { if (JsonSerializer.Deserialize(frame.Data.Span, FleetRpcContext.Default.RpcMessage)?.Error?.Data?.Code == "fleet_protocol_violation") { failures++; } } Assert.InRange(failures, 3, 6); await a.CloseAsync("done", default); // 对端确认关闭之后,会话的接收循环正常结束 await run.WaitAsync(TimeSpan.FromSeconds(5)); } /// 进程内的一对传输:一端发的帧从另一端收到;关闭时对端收到 null。 private sealed class ChannelTransport : IRpcTransport { private readonly Channel _in; private readonly Channel _out; private ChannelTransport(Channel input, Channel output) { _in = input; _out = output; } public static (ChannelTransport A, ChannelTransport B) Pair() { Channel ab = Channel.CreateUnbounded(); Channel ba = Channel.CreateUnbounded(); return (new ChannelTransport(ba, ab), new ChannelTransport(ab, ba)); } public ValueTask SendAsync(RpcFrame frame, CancellationToken ct) { _out.Writer.TryWrite(frame); return ValueTask.CompletedTask; } public async ValueTask ReceiveAsync(CancellationToken ct) => await _in.Reader.WaitToReadAsync(ct) && _in.Reader.TryRead(out RpcFrame? frame) ? frame : null; public ValueTask CloseAsync(string reason, CancellationToken ct) { _out.Writer.TryWrite(null); return ValueTask.CompletedTask; } public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }