Files
MoShiro/backend/tests/MoShiro.Backend.Tests/LinkTests.cs
T
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

117 lines
5.8 KiB
C#

using System.Net;
using System.Text.Json.Nodes;
using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
public sealed class LinkTests : IClassFixture<BackendFixture>
{
private readonly BackendFixture _backend;
public LinkTests(BackendFixture backend)
{
_backend = backend;
}
private async Task<HttpResponseMessage> SetLinkAsync(string dialer, string agent, bool? enabled) =>
await _backend.Admin.PutAsync($"/api/admin/links/{dialer}/{agent}", BackendFixture.Json(new JsonObject { ["enabled"] = enabled }));
private async Task SetAutoLinkAsync(bool on)
{
using HttpResponseMessage response = await _backend.Admin.PatchAsync("/api/admin/settings", BackendFixture.Json(new JsonObject { ["fleet.auto_link"] = on }, "application/merge-patch+json"));
response.EnsureSuccessStatusCode();
}
private async Task<JsonNode?> LinkAsync(string dialer, string agent) =>
(await _backend.GetJsonAsync(_backend.Admin, "/api/admin/links"))["items"]!.AsArray().FirstOrDefault(i => i!["dialer_id"]!.GetValue<string>() == dialer && i["listener_id"]!.GetValue<string>() == agent);
[Fact]
public async Task ListeningAgents_AutomaticallyLinkBothDirections_WithoutSelfLinks()
{
await using AgentSimulator first = await _backend.ConnectAgentAsync();
await using AgentSimulator second = await _backend.ConnectAgentAsync();
string a = first.NodeId.Value;
string b = second.NodeId.Value;
Assert.True((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
Assert.True((await LinkAsync(b, a))!["linked"]!.GetValue<bool>());
Assert.Null(await LinkAsync(a, a));
using HttpResponseMessage self = await SetLinkAsync(a, a, true);
Assert.Equal(HttpStatusCode.UnprocessableEntity, self.StatusCode);
await first.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == b));
await second.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == a));
using HttpResponseMessage off = await SetLinkAsync(a, b, false);
off.EnsureSuccessStatusCode();
Assert.False((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
Assert.True((await LinkAsync(b, a))!["linked"]!.GetValue<bool>());
}
[Fact]
public async Task Links_FollowTheAutomaticSwitch_AndManualChoicesAlwaysWin()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
string dialer = await _backend.CreateDialerIdAsync();
string agentId = agent.NodeId.Value;
try
{
// 自动连接(默认开):新 Dialer 自动进入这个 Agent 允许的名单
await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == dialer));
JsonNode link = (await LinkAsync(dialer, agentId))!;
Assert.True(link["linked"]!.GetValue<bool>());
Assert.Null(link["override"]);
// 手动关掉这一对:自动连接开着也不连
using HttpResponseMessage off = await SetLinkAsync(dialer, agentId, false);
Assert.Equal(HttpStatusCode.OK, off.StatusCode);
await agent.WaitForTaskSetAsync(s => s.Peers.All(p => p.NodeId.Value != dialer));
Assert.False((await LinkAsync(dialer, agentId))!["linked"]!.GetValue<bool>());
// 取消手动设置:回到自动连接
using HttpResponseMessage reset = await SetLinkAsync(dialer, agentId, null);
Assert.Equal(HttpStatusCode.OK, reset.StatusCode);
await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == dialer));
// 关掉自动连接:没有手动选中的就都不连
await SetAutoLinkAsync(false);
await agent.WaitForTaskSetAsync(s => s.Peers.All(p => p.NodeId.Value != dialer));
Assert.False((await _backend.GetJsonAsync(_backend.Admin, "/api/admin/links"))["auto_link"]!.GetValue<bool>());
// 自动连接关着,手动选中的照样连
await SetLinkAsync(dialer, agentId, true);
await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == dialer));
Assert.True((await LinkAsync(dialer, agentId))!["override"]!.GetValue<bool>());
}
finally
{
await SetLinkAsync(dialer, agentId, null);
await SetAutoLinkAsync(true); // 共用的后端:恢复默认,别影响别的测试
}
}
[Fact]
public async Task Links_RefuseUnknownNodes()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
string dialer = await _backend.CreateDialerIdAsync();
using HttpResponseMessage noDialer = await SetLinkAsync("agt_missing", agent.NodeId.Value, false);
Assert.Equal(HttpStatusCode.NotFound, noDialer.StatusCode);
using HttpResponseMessage noAgent = await SetLinkAsync(dialer, "agt_missing", false);
Assert.Equal(HttpStatusCode.NotFound, noAgent.StatusCode);
}
[Fact]
public async Task Links_RetryEnablesPairAndReportsWhenDialerIsOffline()
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
string dialer = await _backend.CreateDialerIdAsync();
using HttpResponseMessage response = await _backend.Admin.PostAsync($"/api/admin/links/{dialer}/{agent.NodeId.Value}/retry", BackendFixture.Json(new JsonObject()));
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
JsonNode body = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
Assert.False(body["queued"]!.GetValue<bool>());
Assert.True(body["link"]!["linked"]!.GetValue<bool>());
Assert.Equal("dialer_unavailable", body["link"]!["connection_state"]!.GetValue<string>());
Assert.Contains("拨号节点状态", body["link"]!["connection_reason"]!.GetValue<string>());
}
}