NetworkProbeLoopbackTests.cs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. using System.Net;
  2. using System.Net.Sockets;
  3. using RackPeek.Domain.Discovery;
  4. using RackPeek.Domain.Resources.Services.Networking;
  5. namespace Tests.Discovery;
  6. /// <summary>
  7. /// The real probe against loopback — the one network every CI runner has and the
  8. /// tests are allowed to touch. TCP carries these tests on purpose: ICMP needs
  9. /// privileges some runners lack, and the scanner's whole point is that liveness
  10. /// never depends on ping alone.
  11. /// </summary>
  12. public class NetworkProbeLoopbackTests {
  13. [Fact]
  14. public async Task A_listening_port_answers_a_connect_probe() {
  15. using var listener = new TcpListener(IPAddress.Loopback, 0);
  16. listener.Start();
  17. var port = ((IPEndPoint)listener.LocalEndpoint).Port;
  18. var probe = new NetworkProbe();
  19. Assert.True(await probe.TryConnectAsync("127.0.0.1", port, TimeSpan.FromSeconds(2)));
  20. }
  21. [Fact]
  22. public async Task A_closed_port_says_no_instead_of_throwing() {
  23. // Bind-then-close guarantees the port exists and nothing is listening on it.
  24. using var listener = new TcpListener(IPAddress.Loopback, 0);
  25. listener.Start();
  26. var port = ((IPEndPoint)listener.LocalEndpoint).Port;
  27. listener.Stop();
  28. var probe = new NetworkProbe();
  29. Assert.False(await probe.TryConnectAsync("127.0.0.1", port, TimeSpan.FromSeconds(2)));
  30. }
  31. [Fact]
  32. public async Task An_unroutable_address_gives_up_within_the_timeout_budget() {
  33. var probe = new NetworkProbe();
  34. DateTime started = DateTime.UtcNow;
  35. // TEST-NET-1 (RFC 5737) is never routed; the connect must die on OUR timer.
  36. var open = await probe.TryConnectAsync("192.0.2.1", 9, TimeSpan.FromMilliseconds(250));
  37. Assert.False(open);
  38. Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(5),
  39. "The connect ignored the timeout and sat on the OS default instead.");
  40. }
  41. [Fact]
  42. public async Task The_whole_scan_pipeline_finds_a_real_listener_on_loopback() {
  43. using var listener = new TcpListener(IPAddress.Loopback, 0);
  44. listener.Start();
  45. var port = ((IPEndPoint)listener.LocalEndpoint).Port;
  46. IReadOnlyList<NetworkHostFact> hosts = await NetworkScanner.ScanAsync(
  47. new NetworkProbe(),
  48. new NetworkScanOptions {
  49. Cidr = Cidr.Parse("127.0.0.1/32"),
  50. Ports = [port],
  51. // Loopback ping may be privilege-blocked on the runner; the open port
  52. // must carry the verdict alone, so keep the ping window tiny.
  53. PingTimeout = TimeSpan.FromMilliseconds(50),
  54. PortTimeout = TimeSpan.FromSeconds(2)
  55. });
  56. NetworkHostFact host = Assert.Single(hosts);
  57. Assert.Equal("127.0.0.1", host.Ip);
  58. Assert.True(host.AnsweredPing || host.OpenPorts.Contains(port));
  59. }
  60. [Fact]
  61. public async Task Scanning_a_dead_block_finds_nothing_and_finishes_quickly() {
  62. // Loopback cannot play the dead host: on Linux the whole 127/8 answers ping.
  63. // TEST-NET-1 (RFC 5737) is reserved and never routed, on every platform.
  64. DateTime started = DateTime.UtcNow;
  65. IReadOnlyList<NetworkHostFact> hosts = await NetworkScanner.ScanAsync(
  66. new NetworkProbe(),
  67. new NetworkScanOptions {
  68. Cidr = Cidr.Parse("192.0.2.0/30"),
  69. Ports = [9],
  70. PingTimeout = TimeSpan.FromMilliseconds(50),
  71. PortTimeout = TimeSpan.FromMilliseconds(250)
  72. });
  73. Assert.Empty(hosts);
  74. Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(10));
  75. }
  76. }