Files
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

99 lines
5.2 KiB
C#

using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using Fleet.Identity;
using Microsoft.Extensions.DependencyInjection;
using MoShiro.Backend.Application;
namespace MoShiro.Backend.Tests;
/// <summary>
/// 每个 监听侧和拨号侧 Agent 都必须有属主(passkey):属主是节点敏感内容(extension 会话、加密数据)绑定的人,装上时固定在节点本地。
/// 没有信任根、没选属主、选了操作员集合之外的凭证,都不能创建加入码;安装脚本和容器入口缺了属主也不装。
/// </summary>
public sealed class OwnerPolicyTests
{
private static OperatorSet Set(params string[] ids) =>
new(1, OperatorTrust.Genesis, "example.test", ["https://example.test"], [.. ids.Select(id => new OperatorKey(id, [1, 2, 3]))]);
[Fact]
public void WithoutATrustRoot_AnOwnerCannotBeChosen_WhenOwnersAreRequired()
{
Assert.False(OwnerPolicy.Resolve(required: true, null, null).IsSuccess);
Assert.False(OwnerPolicy.Resolve(required: true, null, ["a"]).IsSuccess);
Assert.Empty(OwnerPolicy.Resolve(required: false, null, null).Value); // 没有信任根的老部署:不固定属主
}
[Fact]
public void ByDefaultEveryOperatorIsAnOwner_AndAnExplicitChoiceMustBeNonEmptyAndKnown()
{
OperatorSet set = Set("a", "b");
Assert.Equal(["a", "b"], OwnerPolicy.Resolve(true, set, null).Value);
Assert.Equal(["b"], OwnerPolicy.Resolve(true, set, ["b", "b"]).Value);
Assert.False(OwnerPolicy.Resolve(true, set, []).IsSuccess); // 一把都不选:不行
Assert.False(OwnerPolicy.Resolve(true, set, ["a", "stranger"]).IsSuccess); // 不是操作员集合里的凭证
Assert.False(OwnerPolicy.Resolve(false, set, ["stranger"]).IsSuccess);
}
}
public sealed class OwnerEndToEndTests : IDisposable, IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
private readonly BackendOptions _options;
private readonly bool _before;
public OwnerEndToEndTests(BackendFixture backend)
{
_backend = backend;
_options = backend.App.Services.GetRequiredService<BackendOptions>();
_before = _options.RequireOwner;
_options.RequireOwner = true;
}
public void Dispose() => _options.RequireOwner = _before;
[Fact]
public async Task ANodeCannotBeAddedWithoutAnOwner()
{
Operator op = await SharedOperator.GetAsync(_backend); // 共享的操作员:不要释放它
HttpClient session = op.Session;
string known = op.Passkey.Id;
using HttpResponseMessage none = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["controllers"] = new JsonArray() }));
Assert.Equal(HttpStatusCode.BadRequest, none.StatusCode);
Assert.Contains("owner", await none.Content.ReadAsStringAsync(), StringComparison.Ordinal);
using HttpResponseMessage stranger = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["controllers"] = new JsonArray("not-an-operator") }));
Assert.Equal(HttpStatusCode.BadRequest, stranger.StatusCode);
// 明确选了一把,或者不指定(默认是全部操作员):可以,命令里带着创世指纹和属主
using HttpResponseMessage chosen = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent", ["controllers"] = new JsonArray(known) }));
Assert.Equal(HttpStatusCode.Created, chosen.StatusCode);
string linux = JsonNode.Parse(await chosen.Content.ReadAsStringAsync())!["install_commands"]!["linux"]!.GetValue<string>();
Assert.Contains("MOSHIRO_GENESIS=", linux, StringComparison.Ordinal);
Assert.Contains("MOSHIRO_CONTROLLERS=", linux, StringComparison.Ordinal);
Assert.Contains(known, linux, StringComparison.Ordinal);
using HttpResponseMessage defaults = await session.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent" }));
Assert.Equal(HttpStatusCode.Created, defaults.StatusCode);
}
[Fact]
public async Task TheInstallScripts_RefuseANodeWithoutAnOwner()
{
// 脚本是静态的,所以可以直接检查:缺了信任根指纹或属主就不装,只有明确设置 MOSHIRO_ALLOW_NO_OWNER=1(没有信任根的老面板)才放行
using HttpClient anonymous = _backend.NewClient();
foreach (string path in new[] { "/install", "/install.ps1" })
{
string script = await anonymous.GetStringAsync(path);
Assert.Contains("MOSHIRO_ALLOW_NO_OWNER", script, StringComparison.Ordinal);
Assert.Contains("this node has no owner", script, StringComparison.Ordinal);
// 只按容器判断「已经装过」:podman 的 inspect 不分类型,本机残留的旧镜像会被误当成已安装的容器
Assert.Contains("container inspect", script, StringComparison.Ordinal);
Assert.DoesNotContain("inspect \"$container\"", script.Replace("container inspect", ""), StringComparison.Ordinal);
}
}
}