using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// An open port is a service, not a note on the host.
///
/// It used to land in an "open-ports" label — a comma-separated string that
/// nothing could link to, filter on, or hang a note from. RackPeek already has a
/// resource for "a thing listening on an address and a port", so a sweep that
/// finds 22 open on nebula should produce a Service called nebula-ssh running on
/// nebula, exactly as if someone had written it in by hand.
///
///
public class OpenPortServiceTests {
private static List Scan(string ip, string? mac, params int[] ports) =>
NetworkScanMapper.ToResources([
new NetworkHostFact(ip, mac, "nebula", true, ports)
]);
private static List Services(params int[] ports) =>
Scan("192.0.2.20", "bc:24:11:00:2a:01", ports).OfType().ToList();
[Fact]
public void An_open_port_is_no_longer_a_label() {
SystemResource host = Scan("192.0.2.20", "bc:24:11:00:2a:01", 22, 80)
.OfType()
.Single();
Assert.False(host.Labels.ContainsKey("open-ports"));
}
[Fact]
public void Each_open_port_becomes_a_service_on_the_host() {
List services = Services(22, 80);
Assert.Equal(2, services.Count);
Assert.All(services, s => Assert.Equal(["nebula"], s.RunsOn));
}
[Fact]
public void A_service_is_named_for_the_host_and_what_the_port_serves() {
Service ssh = Assert.Single(Services(22));
Assert.Equal("nebula-ssh", ssh.Name);
}
[Theory]
[InlineData(443, "nebula-https")]
[InlineData(445, "nebula-smb")]
[InlineData(1883, "nebula-mqtt")]
[InlineData(8006, "nebula-proxmox")]
[InlineData(32400, "nebula-plex")]
public void Well_known_ports_are_named_by_what_they_serve(int port, string expected) =>
Assert.Equal(expected, Assert.Single(Services(port)).Name);
[Fact]
// Better an honest tcp-9987 than a guess: the number is the only fact available.
public void An_unrecognised_port_keeps_its_number() =>
Assert.Equal("nebula-tcp-9987", Assert.Single(Services(9987)).Name);
[Fact]
public void A_service_that_named_itself_beats_what_its_port_implies() {
// A port is a convention and an answer is evidence. Home Assistant on 8123 is
// the convention; a page that says "Forgejo" is the machine telling you.
List resources = NetworkScanMapper.ToResources([
new NetworkHostFact("192.0.2.21", "bc:24:11:00:2a:02", "nebula", true, [8123]) {
Services = [new ServiceIdentity("Forgejo", IdentitySource.Http, 8123)]
}
]);
Assert.Equal("forgejo", Assert.Single(resources.OfType()).Name);
}
[Fact]
public void A_service_records_where_it_is_listening() {
Service ssh = Assert.Single(Services(22));
Assert.Equal("192.0.2.20", ssh.Network?.Ip);
Assert.Equal(22, ssh.Network?.Port);
Assert.Equal("TCP", ssh.Network?.Protocol);
}
[Fact]
public void A_services_identity_is_its_hosts_identity_and_the_port() {
// Stable across rescans, and distinct per port, so a rescan updates the same two
// cards rather than inventing a pair every time.
var first = Services(22, 80).Select(s => s.DiscoveryId).ToList();
var second = Services(22, 80).Select(s => s.DiscoveryId).ToList();
Assert.Equal(first, second);
Assert.Equal(2, first.Distinct().Count());
}
[Fact]
public void Two_hosts_running_the_same_thing_get_distinct_cards() {
// Naming after the host is what keeps these apart — "ssh" alone would collide on
// every machine in the rack.
List resources = NetworkScanMapper.ToResources([
new NetworkHostFact("192.0.2.30", "bc:24:11:00:2a:03", "nebula", true, [22]),
new NetworkHostFact("192.0.2.31", "bc:24:11:00:2a:04", "orion", true, [22])
]);
Assert.Equal(
["nebula-ssh", "orion-ssh"],
resources.OfType().Select(s => s.Name).Order());
}
[Fact]
public void A_host_with_nothing_listening_yields_no_services() =>
Assert.Empty(Scan("192.0.2.40", "bc:24:11:00:2a:05").OfType());
}