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;
}
}