POST /api/admin/links/reset clears every manual link setting and turns automatic linking on. In the default rules a listening node in CN ranks below a listening node elsewhere, so the CN side dials; the links are pushed again when a node's country is first resolved or changes.
183 lines
9.3 KiB
C#
183 lines
9.3 KiB
C#
using System.Net;
|
|
using System.Text.Json.Nodes;
|
|
using Fleet.Server;
|
|
using MoShiro.Backend.Application;
|
|
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_AutomaticallyShareOneLine_DialedBySmallerId()
|
|
{
|
|
await using AgentSimulator first = await _backend.ConnectAgentAsync();
|
|
await using AgentSimulator second = await _backend.ConnectAgentAsync();
|
|
string[] ids = [.. new[] { first.NodeId.Value, second.NodeId.Value }.Order(StringComparer.Ordinal)];
|
|
(string a, string b) = (ids[0], ids[1]); // a 拨 b
|
|
AgentSimulator listener = a == first.NodeId.Value ? second : first;
|
|
AgentSimulator dialer = a == first.NodeId.Value ? first : second;
|
|
Assert.True((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
|
|
Assert.Null(await LinkAsync(b, a)); // 反方向不再有一条线路
|
|
Assert.Null(await LinkAsync(a, a));
|
|
using HttpResponseMessage self = await SetLinkAsync(a, a, true);
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, self.StatusCode);
|
|
await listener.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == a));
|
|
await dialer.WaitForTaskSetAsync(s => s.Peers.All(p => p.NodeId.Value != b)); // 拨出的一方不放行对端拨进来
|
|
|
|
// 手动让 b 拨 a:线路换方向,仍然只有一条
|
|
using HttpResponseMessage swap = await SetLinkAsync(b, a, true);
|
|
swap.EnsureSuccessStatusCode();
|
|
Assert.Null(await LinkAsync(a, b));
|
|
Assert.True((await LinkAsync(b, a))!["linked"]!.GetValue<bool>());
|
|
await dialer.WaitForTaskSetAsync(s => s.Peers.Any(p => p.NodeId.Value == b));
|
|
|
|
// 关掉这一对:两个方向都不连,线路按默认方向显示,手动设置对这一对生效
|
|
using HttpResponseMessage off = await SetLinkAsync(b, a, false);
|
|
off.EnsureSuccessStatusCode();
|
|
Assert.Null(await LinkAsync(b, a));
|
|
JsonNode closed = (await LinkAsync(a, b))!;
|
|
Assert.False(closed["linked"]!.GetValue<bool>());
|
|
Assert.False(closed["override"]!.GetValue<bool>());
|
|
await SetLinkAsync(b, a, null);
|
|
Assert.True((await LinkAsync(a, b))!["linked"]!.GetValue<bool>());
|
|
}
|
|
|
|
private static AgentRecord Node(string id, bool listens) =>
|
|
new(new Fleet.Core.NodeId(id), id, NodeStatus.Active, [], [], "", 0, [], [], true, [], null, null, 0, 0, null, 0, null, 0, Listens: listens);
|
|
|
|
[Fact]
|
|
public void DefaultDialer_NonListeningDials_ThenDomesticDialsForeign_ThenSmallerId()
|
|
{
|
|
AgentRecord home = Node("agt_a", listens: false);
|
|
AgentRecord cn = Node("agt_b", listens: true);
|
|
AgentRecord abroad = Node("agt_c", listens: true);
|
|
AgentRecord abroad2 = Node("agt_d", listens: true);
|
|
var snapshot = new LinkSnapshot(true, [], ["agt_b"]);
|
|
|
|
Assert.True(snapshot.IsLinked(home, cn)); // 不监听的拨监听的
|
|
Assert.False(snapshot.IsLinked(cn, home));
|
|
Assert.True(snapshot.IsLinked(cn, abroad)); // 国内的拨国外的,哪怕它的 id 更小
|
|
Assert.False(snapshot.IsLinked(abroad, cn));
|
|
Assert.True(snapshot.IsLinked(abroad, abroad2)); // 一样的按 id,小的拨
|
|
Assert.False(snapshot.IsLinked(abroad2, abroad));
|
|
Assert.True(snapshot.IsLinked(home, cn)); // 只有国内的在监听时,国内的也得当监听方
|
|
|
|
// 手动指定的方向不受国家影响:让国外的拨国内的
|
|
var manual = new LinkSnapshot(true, new() { [("agt_c", "agt_b")] = true }, ["agt_b"]);
|
|
Assert.True(manual.IsLinked(abroad, cn));
|
|
Assert.False(manual.IsLinked(cn, abroad));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Links_ResetDropsManualSettingsAndTurnsAutomaticLinkingOn()
|
|
{
|
|
await using AgentSimulator first = await _backend.ConnectAgentAsync();
|
|
await using AgentSimulator second = await _backend.ConnectAgentAsync();
|
|
string[] ids = [.. new[] { first.NodeId.Value, second.NodeId.Value }.Order(StringComparer.Ordinal)];
|
|
await SetLinkAsync(ids[1], ids[0], true); // 手动让大 id 的拨小 id 的
|
|
await SetAutoLinkAsync(false);
|
|
try
|
|
{
|
|
using HttpResponseMessage reset = await _backend.Admin.PostAsync("/api/admin/links/reset", BackendFixture.Json(new JsonObject()));
|
|
reset.EnsureSuccessStatusCode();
|
|
JsonNode matrix = JsonNode.Parse(await reset.Content.ReadAsStringAsync())!;
|
|
Assert.True(matrix["auto_link"]!.GetValue<bool>());
|
|
JsonNode link = (await LinkAsync(ids[0], ids[1]))!; // 回到默认方向:小 id 拨大 id
|
|
Assert.True(link["linked"]!.GetValue<bool>());
|
|
Assert.Null(link["override"]);
|
|
}
|
|
finally
|
|
{
|
|
await SetAutoLinkAsync(true);
|
|
}
|
|
}
|
|
|
|
[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>());
|
|
}
|
|
}
|