Files
MoShiro/backend/tests/MoShiro.Backend.Tests/ProbeEndToEndTests.cs
T
justme 84f9d211bc
ci/woodpecker/pr/verify Pipeline failed
ci/woodpecker/pr/web-build unknown status
backend: require confirmation for signed channel updates [CI]
2026-10-07 12:57:20 +08:00

131 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json.Nodes;
namespace MoShiro.Backend.Tests;
/// <summary>真实的后端 + 真实的 Agent + 真实的 Probe(都在进程内,回环网络)。</summary>
[Collection("probe-e2e")]
public sealed class ProbeEndToEndTests
{
private readonly BackendFixture _backend;
public ProbeEndToEndTests(BackendFixture backend)
{
_backend = backend;
}
private async Task<JsonNode> AgentViewAsync(AgentHarness agent) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id);
private async Task<int> ChunkCountAsync(AgentHarness agent, string kind) =>
(await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Count(c => c!["kind"]!.GetValue<string>() == kind);
[Fact]
public async Task Probe_GetsTheAgentListFromThePanel_RunsTheLineTasks_AndTheBackendStoresMergedChunks()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync(rateHz: 30);
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(agent.Id)), because: "the panel sends the agent list to the probe, which then connects");
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.Connection(agent.Id)!.RoleCount == 2), because: "udp_echo and tcp_echo");
// 每 10 秒切一块,回显类再等 2 秒宽限
await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "udp_echo") >= 1 && await ChunkCountAsync(agent, "tcp_echo") >= 1, TimeSpan.FromSeconds(40));
Assert.True(agent.App.Counters.Fragments >= 2);
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
// 时钟偏移已经估计出来:进程内两端共用同一个单调时钟,所以偏移应当接近 0(真实线路上是两台机器各自的时钟原点之差)
Assert.InRange(probe.App.Connection(agent.Id)!.ClockOffsetUs, -20_000, 20_000);
}
[Fact]
public async Task Probe_IntroducesItselfToThePanel_AndARevokedProbeIsLockedOut()
{
await _backend.SiteMustBePublicAsync();
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
string path = "/api/admin/probes/" + probe.ProbeId;
// 直连面板:在线,hello 里的系统、架构和版本都被记下来了
await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, path))["online"]!.GetValue<bool>(), because: "the probe links to the panel");
await BackendFixture.EventuallyAsync(
async () => (await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/nodes"))["items"]!.AsArray().Any(n => n!["id"]!.GetValue<string>() == probe.ProbeId && n["os"] is not null && n["arch"] is not null),
because: "the platform comes from the probe's own hello");
// 吊销:面板立刻断开它,之后连不上
using HttpResponseMessage revoked = await _backend.Admin.PostAsync(path + "/revoke", null);
revoked.EnsureSuccessStatusCode();
await BackendFixture.EventuallyAsync(async () =>
!(await _backend.GetJsonAsync(_backend.Admin, path))["online"]!.GetValue<bool>() && !probe.App.Link.IsConnected,
because: "a revoked probe is disconnected and cannot come back");
}
[Fact]
public async Task Probe_FollowsTheAgentListThePanelKnows_AndSavesItForTheNextStart()
{
await using AgentHarness first = await AgentHarness.StartAsync(_backend);
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(first.Id)));
await using AgentHarness second = await AgentHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(second.Id)), TimeSpan.FromSeconds(30), "a new agent should be picked up through agents_changed: " + string.Join(";", probe.App.Directory.Agents.Select(a => $"{a.AgentId}@{a.Host}:{a.TcpPort}")) + " v" + probe.App.Directory.Version + " want " + second.Id);
Assert.Contains(probe.App.Directory.Agents, a => a.AgentId == second.Id);
Assert.True(File.Exists(Path.Combine(probe.Directory, "agents.json")));
// 停用一个 Agent:Probe 断开它
using HttpResponseMessage disabled = await _backend.Admin.PostAsync($"/api/admin/agents/{second.Id}/disable", BackendFixture.Json(new JsonObject()));
Assert.True(disabled.IsSuccessStatusCode, await disabled.Content.ReadAsStringAsync());
await BackendFixture.EventuallyAsync(() => Task.FromResult(!probe.App.IsConnected(second.Id)), TimeSpan.FromSeconds(30));
}
[Fact]
public async Task Hy2Ladder_WithTheRealProbe_Succeeds_AndEveryFragmentIsStored()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync();
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "hy2_ladder", ["params"] = new JsonObject { ["size"] = 1000, ["duration_s"] = 5 } }));
Assert.True(accepted.StatusCode == System.Net.HttpStatusCode.Accepted, await accepted.Content.ReadAsStringAsync());
string testId = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue<string>();
JsonNode test = null!;
await BackendFixture.EventuallyAsync(
async () =>
{
test = await _backend.GetJsonAsync(_backend.Admin, "/api/public/tests/" + testId);
return test["status"]!.GetValue<string>() is "succeeded" or "failed" or "aborted";
},
TimeSpan.FromSeconds(90));
Assert.True(test["status"]!.GetValue<string>() == "succeeded", test.ToJsonString());
await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "udp_train") >= 5, because: "the 100 Mbit/s phase (5 s of 1000 B packets, over 50000 records) is split into two fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
[Fact]
public async Task RealitySession_WithTheRealProbe_RunsTcpBulkAndStoresBytesAndKernelRows()
{
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync();
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue<bool>());
using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" }));
Assert.True(accepted.StatusCode == System.Net.HttpStatusCode.Accepted, await accepted.Content.ReadAsStringAsync());
string testId = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue<string>();
JsonNode test = null!;
await BackendFixture.EventuallyAsync(
async () =>
{
test = await _backend.GetJsonAsync(_backend.Admin, "/api/public/tests/" + testId);
return test["status"]!.GetValue<string>() is "succeeded" or "failed" or "aborted";
},
TimeSpan.FromSeconds(150));
Assert.True(test["status"]!.GetValue<string>() == "succeeded", test.ToJsonString());
await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "tcp_bulk") >= 1, because: "the bulk phase stores its fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
}