4
0

OpenPortServiceTests.cs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. using RackPeek.Domain.Discovery;
  2. using RackPeek.Domain.Resources;
  3. using RackPeek.Domain.Resources.Services;
  4. using RackPeek.Domain.Resources.SystemResources;
  5. namespace Tests.Discovery;
  6. /// <summary>
  7. /// An open port is a service, not a note on the host.
  8. /// <para>
  9. /// It used to land in an "open-ports" label — a comma-separated string that
  10. /// nothing could link to, filter on, or hang a note from. RackPeek already has a
  11. /// resource for "a thing listening on an address and a port", so a sweep that
  12. /// finds 22 open on nebula should produce a Service called nebula-ssh running on
  13. /// nebula, exactly as if someone had written it in by hand.
  14. /// </para>
  15. /// </summary>
  16. public class OpenPortServiceTests {
  17. private static List<Resource> Scan(string ip, string? mac, params int[] ports) =>
  18. NetworkScanMapper.ToResources([
  19. new NetworkHostFact(ip, mac, "nebula", true, ports)
  20. ]);
  21. private static List<Service> Services(params int[] ports) =>
  22. Scan("192.0.2.20", "bc:24:11:00:2a:01", ports).OfType<Service>().ToList();
  23. [Fact]
  24. public void An_open_port_is_no_longer_a_label() {
  25. SystemResource host = Scan("192.0.2.20", "bc:24:11:00:2a:01", 22, 80)
  26. .OfType<SystemResource>()
  27. .Single();
  28. Assert.False(host.Labels.ContainsKey("open-ports"));
  29. }
  30. [Fact]
  31. public void Each_open_port_becomes_a_service_on_the_host() {
  32. List<Service> services = Services(22, 80);
  33. Assert.Equal(2, services.Count);
  34. Assert.All(services, s => Assert.Equal(["nebula"], s.RunsOn));
  35. }
  36. [Fact]
  37. public void A_service_is_named_for_the_host_and_what_the_port_serves() {
  38. Service ssh = Assert.Single(Services(22));
  39. Assert.Equal("nebula-ssh", ssh.Name);
  40. }
  41. [Theory]
  42. [InlineData(443, "nebula-https")]
  43. [InlineData(445, "nebula-smb")]
  44. [InlineData(1883, "nebula-mqtt")]
  45. [InlineData(8006, "nebula-proxmox")]
  46. [InlineData(32400, "nebula-plex")]
  47. public void Well_known_ports_are_named_by_what_they_serve(int port, string expected) =>
  48. Assert.Equal(expected, Assert.Single(Services(port)).Name);
  49. [Fact]
  50. // Better an honest tcp-9987 than a guess: the number is the only fact available.
  51. public void An_unrecognised_port_keeps_its_number() =>
  52. Assert.Equal("nebula-tcp-9987", Assert.Single(Services(9987)).Name);
  53. [Fact]
  54. public void A_service_that_named_itself_beats_what_its_port_implies() {
  55. // A port is a convention and an answer is evidence. Home Assistant on 8123 is
  56. // the convention; a page that says "Forgejo" is the machine telling you.
  57. List<Resource> resources = NetworkScanMapper.ToResources([
  58. new NetworkHostFact("192.0.2.21", "bc:24:11:00:2a:02", "nebula", true, [8123]) {
  59. Services = [new ServiceIdentity("Forgejo", IdentitySource.Http, 8123)]
  60. }
  61. ]);
  62. Assert.Equal("forgejo", Assert.Single(resources.OfType<Service>()).Name);
  63. }
  64. [Fact]
  65. public void A_service_records_where_it_is_listening() {
  66. Service ssh = Assert.Single(Services(22));
  67. Assert.Equal("192.0.2.20", ssh.Network?.Ip);
  68. Assert.Equal(22, ssh.Network?.Port);
  69. Assert.Equal("TCP", ssh.Network?.Protocol);
  70. }
  71. [Fact]
  72. public void A_services_identity_is_its_hosts_identity_and_the_port() {
  73. // Stable across rescans, and distinct per port, so a rescan updates the same two
  74. // cards rather than inventing a pair every time.
  75. var first = Services(22, 80).Select(s => s.DiscoveryId).ToList();
  76. var second = Services(22, 80).Select(s => s.DiscoveryId).ToList();
  77. Assert.Equal(first, second);
  78. Assert.Equal(2, first.Distinct().Count());
  79. }
  80. [Fact]
  81. public void Two_hosts_running_the_same_thing_get_distinct_cards() {
  82. // Naming after the host is what keeps these apart — "ssh" alone would collide on
  83. // every machine in the rack.
  84. List<Resource> resources = NetworkScanMapper.ToResources([
  85. new NetworkHostFact("192.0.2.30", "bc:24:11:00:2a:03", "nebula", true, [22]),
  86. new NetworkHostFact("192.0.2.31", "bc:24:11:00:2a:04", "orion", true, [22])
  87. ]);
  88. Assert.Equal(
  89. ["nebula-ssh", "orion-ssh"],
  90. resources.OfType<Service>().Select(s => s.Name).Order());
  91. }
  92. [Fact]
  93. public void A_host_with_nothing_listening_yields_no_services() =>
  94. Assert.Empty(Scan("192.0.2.40", "bc:24:11:00:2a:05").OfType<Service>());
  95. }