Files
MoShiro/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs
T
justme 1ff847e45a
ci/woodpecker/push/main-amd64-build Pipeline was successful
feat: declare every protocol method's direction and enforce it in the router; add the zero-trust design [CI]
Methods, roles and binary channels are declared in one table per plane (node and peer). RpcRouter checks direction, kind and role when handlers are registered, when messages arrive (fleet_protocol_violation, disconnect after repeated violations) and before sending. NodeConnection is split by responsibility.
2026-10-07 16:17:02 +08:00

95 lines
4.0 KiB
C#

using System.Reflection;
using Fleet.Rpc;
using Fleet.Server;
using Microsoft.Extensions.DependencyInjection;
using MoShiro.Probe;
using MoShiro.Protocol;
namespace MoShiro.Backend.Tests;
/// <summary>
/// 协议方向声明(<see cref="MoShiroMethodTable"/>)和实际代码的对账:每个方法常量都声明了,每一侧该处理的方法都注册了处理函数,
/// 二进制通道都有对应的收件箱,文档里写了每一个方法。新增方法绕不过声明,声明了没人处理也会被发现。
/// 注册处理函数时路由器本身还会核对方向、类型和角色,不符就在启动时失败。
/// </summary>
[Collection("probe-e2e")]
public sealed class ProtocolDeclarationTests
{
private static readonly MethodTable s_table = MoShiroMethodTable.Table;
private readonly BackendFixture _backend;
public ProtocolDeclarationTests(BackendFixture backend)
{
_backend = backend;
}
private static IEnumerable<string> ConstantsOf(Type type) =>
type.GetFields(BindingFlags.Public | BindingFlags.Static).Where(f => f.IsLiteral && f.FieldType == typeof(string)).Select(f => (string)f.GetRawConstantValue()!);
[Fact]
public void EveryMethodConstant_IsDeclared()
{
HashSet<string> declared = [.. s_table.Methods.Select(m => m.Name)];
string[] undeclared = [.. ConstantsOf(typeof(FleetMethods)).Concat(ConstantsOf(typeof(PeerMethods))).Concat(ConstantsOf(typeof(MoShiroMethods))).Where(n => !declared.Contains(n))];
Assert.Empty(undeclared);
}
[Fact]
public void EveryDeclaredMethod_IsDocumented()
{
string root = FindRoot();
string docs = File.ReadAllText(Path.Combine(root, "docs", "protocol.md")) + File.ReadAllText(Path.Combine(root, "backend", "fleet", "README.md"));
string[] missing = [.. s_table.Methods.Select(m => m.Name).Distinct().Where(n => !docs.Contains($"`{n}`", StringComparison.Ordinal))];
Assert.Empty(missing);
}
[Fact]
public void Backend_HandlesEveryMethodNodesMayInitiate_AndHasAnInboxForEveryDeclaredChannel()
{
FleetRouter router = _backend.Services.GetRequiredService<FleetRouter>();
Assert.Empty(router.Table.MissingHandlers(RpcSide.Server, null, router.Methods));
byte[] inboxes = [.. _backend.Services.GetServices<IInbox>().Select(i => i.Channel).Order()];
byte[] declared = [.. s_table.Channels.Where(c => c.Plane == RpcPlane.Node).Select(c => c.Channel).Order()];
Assert.Equal(declared, inboxes);
}
[Fact]
public async Task Agent_HandlesEveryMethodItShould_OnBothPlanes()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Agent, agent.App.Link.Router.Methods));
// 对等面:Agent 是监听方,要处理 Probe 发来的方法
Assert.Empty(s_table.MissingHandlers(RpcSide.Listener, null, agent.App.PeerRouter.Methods));
}
[Fact]
public async Task Probe_HandlesEveryMethodItShould_OnBothPlanes()
{
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Probe, probe.App.Link.Router.Methods));
// 对等面:Probe 是拨号方,要处理 Agent 发来的方法
Assert.Empty(s_table.MissingHandlers(RpcSide.Dialer, null, AgentConnection.Router.Methods));
}
[Fact]
public void RolesOfNodeIds_FollowThePrefix()
{
Assert.Equal(NodeRoles.Agent, NodeRoles.Of(AgentIds.Prefix + "_01ABC"));
Assert.Equal(NodeRoles.Probe, NodeRoles.Of(ProbeId.Prefix + "_01ABC"));
Assert.Null(NodeRoles.Of("usr_01ABC"));
}
private static string FindRoot()
{
string? dir = AppContext.BaseDirectory;
while (dir is not null && !File.Exists(Path.Combine(dir, "docs", "protocol.md")))
{
dir = Path.GetDirectoryName(dir);
}
return dir ?? throw new InvalidOperationException("repository root not found");
}
}