using System.Net; using System.Text.Json.Nodes; using MoShiro.TestSupport; namespace MoShiro.Backend.Tests; public sealed class LinkTests : IClassFixture { private readonly BackendFixture _backend; public LinkTests(BackendFixture backend) { _backend = backend; } private async Task SetLinkAsync(string probe, string agent, bool? enabled) => await _backend.Admin.PutAsync($"/api/admin/links/{probe}/{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 LinkAsync(string probe, string agent) => (await _backend.GetJsonAsync(_backend.Admin, "/api/admin/links"))["items"]!.AsArray().FirstOrDefault(i => i!["probe_id"]!.GetValue() == probe && i["agent_id"]!.GetValue() == agent); [Fact] public async Task Links_FollowTheAutomaticSwitch_AndManualChoicesAlwaysWin() { await using AgentSimulator agent = await _backend.ConnectAgentAsync(); string probe = await _backend.CreateProbeIdAsync(); string agentId = agent.NodeId.Value; try { // 自动连接(默认开):新 Probe 自动进入这个 Agent 允许的名单 await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == probe)); JsonNode link = (await LinkAsync(probe, agentId))!; Assert.True(link["linked"]!.GetValue()); Assert.Null(link["override"]); // 手动关掉这一对:自动连接开着也不连 using HttpResponseMessage off = await SetLinkAsync(probe, agentId, false); Assert.Equal(HttpStatusCode.OK, off.StatusCode); await agent.WaitForTaskSetAsync(s => s.Peers.All(p => p.ProbeId.Value != probe)); Assert.False((await LinkAsync(probe, agentId))!["linked"]!.GetValue()); // 取消手动设置:回到自动连接 using HttpResponseMessage reset = await SetLinkAsync(probe, agentId, null); Assert.Equal(HttpStatusCode.OK, reset.StatusCode); await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == probe)); // 关掉自动连接:没有手动选中的就都不连 await SetAutoLinkAsync(false); await agent.WaitForTaskSetAsync(s => s.Peers.All(p => p.ProbeId.Value != probe)); Assert.False((await _backend.GetJsonAsync(_backend.Admin, "/api/admin/links"))["auto_link"]!.GetValue()); // 自动连接关着,手动选中的照样连 await SetLinkAsync(probe, agentId, true); await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == probe)); Assert.True((await LinkAsync(probe, agentId))!["override"]!.GetValue()); } finally { await SetLinkAsync(probe, agentId, null); await SetAutoLinkAsync(true); // 共用的后端:恢复默认,别影响别的测试 } } [Fact] public async Task Links_RefuseUnknownNodes() { await using AgentSimulator agent = await _backend.ConnectAgentAsync(); string probe = await _backend.CreateProbeIdAsync(); using HttpResponseMessage noProbe = await SetLinkAsync("prb_missing", agent.NodeId.Value, false); Assert.Equal(HttpStatusCode.NotFound, noProbe.StatusCode); using HttpResponseMessage noAgent = await SetLinkAsync(probe, "agt_missing", false); Assert.Equal(HttpStatusCode.NotFound, noAgent.StatusCode); } [Fact] public async Task Links_RetryEnablesPairAndReportsWhenProbeIsOffline() { await using AgentSimulator agent = await _backend.ConnectAgentAsync(); string probe = await _backend.CreateProbeIdAsync(); using HttpResponseMessage response = await _backend.Admin.PostAsync($"/api/admin/links/{probe}/{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()); Assert.True(body["link"]!["linked"]!.GetValue()); Assert.Equal("probe_unavailable", body["link"]!["connection_state"]!.GetValue()); Assert.Contains("Probe 状态", body["link"]!["connection_reason"]!.GetValue()); } }