using System.Text.Json.Nodes; using Monitor.Agent; using Monitor.Protocol; using Monitor.TestSupport; namespace Monitor.Backend.Tests; public sealed class HostSamplerTests { private const string Stat1 = "cpu 100 0 100 700 100 0 0 0 0 0\ncpu0 1 1 1 1 1 0 0 0 0 0\n"; private const string Stat2 = "cpu 200 0 200 800 100 0 0 0 0 0\n"; // 比上一次:总时间 +300,其中空闲 +100 private const string Dev1 = "Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n lo: 999 1 0 0 0 0 0 0 999 1 0 0 0 0 0 0\n eth0: 1000 10 0 0 0 0 0 0 5000 10 0 0 0 0 0 0\ndocker0: 777 1 0 0 0 0 0 0 777 1 0 0 0 0 0 0\n"; private const string Dev2 = "Inter-| Receive\n face |bytes\n eth0: 3000 10 0 0 0 0 0 0 9000 10 0 0 0 0 0 0\n"; [Fact] public void Parsers_ReadTheProcFormats_AndSkipLoopbackAndContainerInterfaces() { Assert.Equal((1000L, 800L), HostSampler.ParseCpu(Stat1)); Assert.Equal((8L * 1024 * 1024, 2L * 1024 * 1024), HostSampler.ParseMemory("MemTotal: 8192 kB\nMemFree: 100 kB\nMemAvailable: 6144 kB\n")); Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1)); Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n")); Assert.Null(HostSampler.ParseNetwork("nothing useful")); } [Fact] public void Sample_DerivesCpuPercentAndNetworkSpeedFromTheDifferenceBetweenTwoReads() { var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider(); string stat = Stat1; string dev = Dev1; var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250)); HostSnapshot first = sampler.Sample(); Assert.Null(first.CpuPercent); // 第一次没有基线 Assert.Null(first.NetRxBps); Assert.Equal(250, first.DiskUsed); Assert.Equal(100, first.UptimeSeconds); time.Advance(TimeSpan.FromSeconds(2)); stat = Stat2; dev = Dev2; HostSnapshot second = sampler.Sample(); Assert.Equal(66.7, second.CpuPercent); // 总时间 +300、空闲 +100:忙碌 200/300 Assert.Equal(1000, second.NetRxBps); // (3000 - 1000) / 2 s Assert.Equal(2000, second.NetTxBps); Assert.Equal(3000, second.NetRxTotal); } } [Collection("backend")] public sealed class HostStatusTests { private readonly BackendFixture _backend; public HostStatusTests(BackendFixture backend) { _backend = backend; } private static HostSnapshot Snapshot(double cpu) => new() { CpuPercent = cpu, MemUsed = 2L << 30, MemTotal = 8L << 30, DiskUsed = 10L << 30, DiskTotal = 40L << 30, NetRxTotal = 600L << 30, NetTxTotal = 180L << 30, NetRxBps = 2500, NetTxBps = 4800, UptimeSeconds = 3_000_000, }; [Fact] public async Task AgentHostSnapshots_AreVisibleToTheDisplayInRealTime_StoredAsHistory_AndNeverCarryModelsOrAddresses() { await _backend.SiteMustBePublicAsync(); await using AgentSimulator agent = await _backend.ConnectAgentAsync(); string id = agent.NodeId.Value; await using SseClient sse = await SseClient.ConnectAsync(_backend.Anonymous, "/api/public/live?topics=hosts"); await agent.SendHostAsync(Snapshot(18.5)); SseEvent live = await sse.WaitForAsync(e => e.Name == "host" && e.Data!["agent_id"]!.GetValue() == id); Assert.Equal(18.5, live.Data!["host"]!["cpu_percent"]!.GetValue()); Assert.True(live.Data["online"]!.GetValue()); // REST:最新值,没有过期 JsonNode list = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts"); JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue() == id)!; Assert.False(item["stale"]!.GetValue()); Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue()); // 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段 string text = list.ToJsonString(); foreach (string forbidden in new[] { "model", "hostname", "\"ip\"", "address", "remote", "mac", "iface", "public_host" }) { Assert.DoesNotContain(forbidden, text, StringComparison.OrdinalIgnoreCase); } // 线路详情也不泄漏 Probe 的地址和版本 string line = (await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/lines/{_backend.Line(id)}")).ToJsonString(); Assert.DoesNotContain("remote", line, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("probe_version", line, StringComparison.OrdinalIgnoreCase); // 历史:每分钟一个样本 JsonNode history = null!; await BackendFixture.EventuallyAsync(async () => { history = await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/agents/{id}/host?range=1h"); return history["values"]!["cpu"]!.AsArray().Any(v => v is not null); }); Assert.Equal(25.0, history["values"]!["mem"]!.AsArray().Last(v => v is not null)!.GetValue()); Assert.Equal(2500, history["values"]!["rx_bps"]!.AsArray().Last(v => v is not null)!.GetValue()); // 隐藏的 Agent 对匿名访客不可见 await _backend.Admin.PatchAsync($"/api/admin/agents/{id}", BackendFixture.Json(new JsonObject { ["public_visible"] = false })); JsonNode hidden = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts"); Assert.DoesNotContain(hidden["items"]!.AsArray(), i => i!["agent_id"]!.GetValue() == id); using HttpResponseMessage denied = await _backend.Anonymous.GetAsync($"/api/public/agents/{id}/host"); Assert.Equal(System.Net.HttpStatusCode.NotFound, denied.StatusCode); } }