using RackPeek.Domain.Discovery; namespace Tests.Discovery; /// /// The ARP table is where a scanned host's identity comes from, and the two /// platforms print it differently — most dangerously, macOS drops leading zeros /// from MAC octets. If normalisation slips, the same machine gets a different /// discovery id depending on which workstation ran the scan. /// public class ArpTableParserTests { [Fact] public void The_linux_proc_file_parses_to_normalised_macs() { IReadOnlyDictionary table = ArpTableParser.Parse(Fixture.Read("linux-arp-table")); Assert.Equal("a4:91:b1:4e:3c:20", table["192.168.1.1"]); // Uppercase in the fixture, stored lowercase. Assert.Equal("dc:a6:32:0f:11:22", table["192.168.1.20"]); } [Fact] public void The_macos_arp_output_parses_to_the_same_macs_as_linux() { IReadOnlyDictionary linux = ArpTableParser.Parse(Fixture.Read("linux-arp-table")); IReadOnlyDictionary macos = ArpTableParser.Parse(Fixture.Read("macos-arp-output")); // The macOS fixture prints 192.168.1.20 as dc:a6:32:f:11:22 — unpadded. Identity // must not depend on which of the two formats happened to report the machine. Assert.Equal(linux["192.168.1.1"], macos["192.168.1.1"]); Assert.Equal(linux["192.168.1.20"], macos["192.168.1.20"]); } [Fact] public void The_windows_arp_output_parses_to_the_same_macs_as_linux() { IReadOnlyDictionary linux = ArpTableParser.Parse(Fixture.Read("linux-arp-table")); IReadOnlyDictionary windows = ArpTableParser.Parse(Fixture.Read("windows-arp-output")); // Windows prints dashes and uppercase; the interface/header lines parse to nothing. Assert.Equal(linux["192.168.1.1"], windows["192.168.1.1"]); Assert.Equal(linux["192.168.1.20"], windows["192.168.1.20"]); Assert.False(windows.ContainsKey("Interface:")); } [Fact] public void Unresolved_neighbours_contribute_nothing() { IReadOnlyDictionary linux = ArpTableParser.Parse(Fixture.Read("linux-arp-table")); IReadOnlyDictionary macos = ArpTableParser.Parse(Fixture.Read("macos-arp-output")); // Linux marks failures with flags 0x0 or an all-zero MAC; macOS prints "(incomplete)". Assert.False(linux.ContainsKey("192.168.1.50")); Assert.False(linux.ContainsKey("192.168.1.60")); Assert.False(macos.ContainsKey("192.168.1.50")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("not an arp table at all")] [InlineData("IP address HW type Flags HW address Mask Device")] [InlineData("? (garbage at nothing")] public void Garbage_input_is_an_empty_table_not_an_exception(string? text) => Assert.Empty(ArpTableParser.Parse(text)); [Theory] [InlineData("A4:91:B1:4E:3C:20", "a4:91:b1:4e:3c:20")] [InlineData("1:0:5e:0:0:fb", "01:00:5e:00:00:fb")] [InlineData("dc:a6:32:f:11:22", "dc:a6:32:0f:11:22")] [InlineData("A4-91-B1-4E-3C-20", "a4:91:b1:4e:3c:20")] // Windows separators public void Macs_normalise_to_lowercase_padded_octets(string raw, string expected) => Assert.Equal(expected, ArpTableParser.NormaliseMac(raw)); [Theory] [InlineData(null)] [InlineData("")] [InlineData("00:00:00:00:00:00")] // the kernel's "never answered" [InlineData("a4:91:b1:4e:3c")] // five octets [InlineData("a4:91:b1:4e:3c:20:ff")] // seven octets [InlineData("zz:91:b1:4e:3c:20")] // not hex [InlineData("(incomplete)")] public void Anything_that_is_not_a_usable_mac_is_null(string? raw) => Assert.Null(ArpTableParser.NormaliseMac(raw)); }