using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// Swept hosts → the System cards RackPeek stores. The contract that matters most
/// is identity: MAC-seeded, format-independent, IP only as a last resort.
///
public class NetworkScanMapperTests {
private static NetworkHostFact Host(
string ip = "192.168.1.20",
string? mac = "dc:a6:32:0f:11:22",
string? hostname = "nas01.lan") =>
new(ip, mac, hostname, true, []);
[Fact]
public void A_host_becomes_a_system_card_with_ip_mac_and_its_dns_name() {
List resources = NetworkScanMapper.ToResources([Host()]);
SystemResource system = Assert.IsType(Assert.Single(resources));
Assert.Equal("nas01", system.Name); // first label of nas01.lan
Assert.Equal("System", system.Kind);
Assert.Equal("192.168.1.20", system.Ip);
// Deliberately sparse: anything a scan cannot see stays null so a rescan can
// never overwrite what the user or an agent collector filled in.
Assert.Null(system.Type);
Assert.Null(system.Os);
Assert.Null(system.Cores);
Assert.Equal("dc:a6:32:0f:11:22", system.Labels["mac"]);
Assert.StartsWith("rpk1:net:", system.DiscoveryId);
}
[Fact]
public void Identity_rides_on_the_mac_so_a_dhcp_move_is_the_same_machine() {
List before = NetworkScanMapper.ToResources([Host(ip: "192.168.1.20")]);
List after = NetworkScanMapper.ToResources([Host(ip: "192.168.1.99")]);
Assert.Equal(before[0].DiscoveryId, after[0].DiscoveryId);
}
[Fact]
public void Without_a_mac_the_ip_seeds_the_identity_instead() {
List resources = NetworkScanMapper.ToResources([Host(mac: null)]);
Assert.StartsWith("rpk1:net:", resources[0].DiscoveryId);
Assert.False(Assert.IsType(resources[0]).Labels.ContainsKey("mac"));
// ...and it is a different identity than the MAC would have produced.
Assert.NotEqual(
NetworkScanMapper.ToResources([Host()])[0].DiscoveryId,
resources[0].DiscoveryId);
}
[Fact]
public void A_host_with_no_dns_name_gets_a_deterministic_one_from_its_id() {
List resources = NetworkScanMapper.ToResources([Host(hostname: null)]);
Assert.StartsWith("host-", resources[0].Name);
// Deterministic: the same machine names itself the same way on every run.
Assert.Equal(resources[0].Name, NetworkScanMapper.ToResources([Host(hostname: null)])[0].Name);
}
[Fact]
public void Two_hosts_answering_to_the_same_dns_name_stay_distinct() {
// A lazy resolver that answers every PTR with the router's name must not
// collapse the whole network into one card (the import rejects duplicates).
List resources = NetworkScanMapper.ToResources([
Host(ip: "192.168.1.1", mac: "a4:91:b1:4e:3c:20", hostname: "router.lan"),
Host(ip: "192.168.1.2", mac: "b0:00:00:00:00:02", hostname: "router.lan")
]);
Assert.Equal(2, resources.Select(r => r.Name).Distinct(StringComparer.OrdinalIgnoreCase).Count());
Assert.Equal("router", resources[0].Name);
Assert.StartsWith("router-", resources[1].Name);
}
[Fact]
public void One_mac_answering_on_several_addresses_is_one_machine_with_one_card() {
// Gateways answer on VIPs and aliases all the time: one MAC, many addresses —
// still one box. Collapsing keeps the import happy (duplicate ids are rejected)
// AND keeps identity independent of how many addresses answered this scan.
List resources = NetworkScanMapper.ToResources([
Host(ip: "192.168.1.2", hostname: null), // the VIP, deliberately first
Host(ip: "192.168.1.1", hostname: "gw.lan")
]);
SystemResource card = Assert.IsType(Assert.Single(resources));
Assert.Equal("192.168.1.1", card.Ip); // the lowest address, deterministically
Assert.Equal("gw", card.Name); // the one name anywhere in the group
Assert.Equal("192.168.1.1,192.168.1.2", card.Labels["ips"]);
}
[Fact]
public void A_vip_appearing_or_disappearing_never_moves_the_machines_identity() {
// The regression that motivated the collapse: an id seeded on scan-local
// address counts flips when a keepalived VIP fails over. MAC alone, always.
List alone = NetworkScanMapper.ToResources([
Host(ip: "192.168.1.1", hostname: "gw.lan")
]);
List withVip = NetworkScanMapper.ToResources([
Host(ip: "192.168.1.1", hostname: "gw.lan"),
Host(ip: "192.168.1.2", hostname: null)
]);
Assert.Equal(alone[0].DiscoveryId, Assert.Single(withVip).DiscoveryId);
}
[Fact]
public void The_emitted_document_conforms_to_the_published_schema() {
List resources = NetworkScanMapper.ToResources([
Host(),
Host(ip: "192.168.1.30", mac: null, hostname: null)
]);
Fixture.AssertConformsToSchema(DiscoveryDocument.ToYaml(resources));
}
}