using System.Net; using System.Net.Sockets; using RackPeek.Domain.Discovery; using RackPeek.Domain.Resources.Services.Networking; namespace Tests.Discovery; /// /// The real probe against loopback — the one network every CI runner has and the /// tests are allowed to touch. TCP carries these tests on purpose: ICMP needs /// privileges some runners lack, and the scanner's whole point is that liveness /// never depends on ping alone. /// public class NetworkProbeLoopbackTests { [Fact] public async Task A_listening_port_answers_a_connect_probe() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; var probe = new NetworkProbe(); Assert.True(await probe.TryConnectAsync("127.0.0.1", port, TimeSpan.FromSeconds(2))); } [Fact] public async Task A_closed_port_says_no_instead_of_throwing() { // Bind-then-close guarantees the port exists and nothing is listening on it. using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); var probe = new NetworkProbe(); Assert.False(await probe.TryConnectAsync("127.0.0.1", port, TimeSpan.FromSeconds(2))); } [Fact] public async Task An_unroutable_address_gives_up_within_the_timeout_budget() { var probe = new NetworkProbe(); DateTime started = DateTime.UtcNow; // TEST-NET-1 (RFC 5737) is never routed; the connect must die on OUR timer. var open = await probe.TryConnectAsync("192.0.2.1", 9, TimeSpan.FromMilliseconds(250)); Assert.False(open); Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(5), "The connect ignored the timeout and sat on the OS default instead."); } [Fact] public async Task The_whole_scan_pipeline_finds_a_real_listener_on_loopback() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; IReadOnlyList hosts = await NetworkScanner.ScanAsync( new NetworkProbe(), new NetworkScanOptions { Cidr = Cidr.Parse("127.0.0.1/32"), Ports = [port], // Loopback ping may be privilege-blocked on the runner; the open port // must carry the verdict alone, so keep the ping window tiny. PingTimeout = TimeSpan.FromMilliseconds(50), PortTimeout = TimeSpan.FromSeconds(2) }); NetworkHostFact host = Assert.Single(hosts); Assert.Equal("127.0.0.1", host.Ip); Assert.True(host.AnsweredPing || host.OpenPorts.Contains(port)); } [Fact] public async Task Scanning_a_dead_block_finds_nothing_and_finishes_quickly() { // Loopback cannot play the dead host: on Linux the whole 127/8 answers ping. // TEST-NET-1 (RFC 5737) is reserved and never routed, on every platform. DateTime started = DateTime.UtcNow; IReadOnlyList hosts = await NetworkScanner.ScanAsync( new NetworkProbe(), new NetworkScanOptions { Cidr = Cidr.Parse("192.0.2.0/30"), Ports = [9], PingTimeout = TimeSpan.FromMilliseconds(50), PortTimeout = TimeSpan.FromMilliseconds(250) }); Assert.Empty(hosts); Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(10)); } }