using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// Giving a service the host it is plainly running on, when the collector could not.
/// A docker collector talking to a remote engine has to guess its host's name and the
/// link then dangles; the service's own address is evidence the guess is not. The
/// rules that keep this from doing harm are what these tests own.
///
public class RunsOnByIpTests {
private static SystemResource System(string name, string? ip, string? discoveryId = null) =>
new() {
Kind = SystemResource.KindLabel,
Name = name,
Ip = ip,
DiscoveryId = discoveryId
};
private static Service Service(string name, string? ip, string? runsOn = null, string? discoveryId = "rpk1:docker:1") =>
new() {
Kind = RackPeek.Domain.Resources.Services.Service.KindLabel,
Name = name,
DiscoveryId = discoveryId,
Network = ip == null ? null : new Network { Ip = ip },
RunsOn = runsOn == null ? [] : [runsOn]
};
[Fact]
public void A_service_whose_host_name_matches_nothing_is_anchored_to_the_system_at_its_address() {
// The remote-docker case: the engine called itself NAS01.lan, the inventory
// calls that machine nas01, and nothing linked the two.
List existing = [System("nas01", "192.0.2.50")];
List incoming = [Service("immich", "192.0.2.50", "NAS01.lan")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["nas01"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void A_service_with_no_host_at_all_is_anchored_too() {
List existing = [System("nas01", "192.0.2.50")];
List incoming = [Service("immich", "192.0.2.50")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["nas01"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void A_host_arriving_in_the_same_payload_counts() {
// discover docker emits the host alongside its services.
List incoming = [
System("nas01", "192.0.2.50", "rpk1:sys:a"),
Service("immich", "192.0.2.50", "somewhere-else")
];
DiscoveryIdResolver.ResolveNames([], incoming);
Assert.Equal(["nas01"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void A_working_host_link_is_never_overwritten() {
// The service says it runs on a resource that exists. That is the collector's
// own answer and it beats an inference drawn from an address.
List existing = [
System("nas01", "192.0.2.50"),
System("some-vm", "192.0.2.99")
];
List incoming = [Service("immich", "192.0.2.50", "some-vm")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["some-vm"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void A_stored_host_beats_a_scanned_stand_in_at_the_same_address() {
// Once a hypervisor reports its guests' addresses, a sweep of that subnet finds
// the same machines again and contributes a sparse card per address. The service
// belongs on the guest the hypervisor described, not on the sweep's stand-in.
List existing = [System("app-vm", "192.0.2.50")];
List incoming = [
System("host-1a2b3c4d", "192.0.2.50", "rpk1:net:c"),
Service("immich", "192.0.2.50", "SOMEWHERE.lan")
];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["app-vm"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void An_address_two_systems_claim_anchors_nothing() {
// Overlapping subnets across sites, or a stale card nobody cleaned up. An
// ambiguous address is no evidence, and a wrong parent is worse than none.
List existing = [
System("site-a-host", "192.0.2.50"),
System("site-b-host", "192.0.2.50")
];
List incoming = [Service("immich", "192.0.2.50", "NAS01.lan")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["NAS01.lan"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void A_service_with_no_address_is_left_alone() {
List existing = [System("nas01", "192.0.2.50")];
List incoming = [Service("immich", null, "NAS01.lan")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["NAS01.lan"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void An_address_no_system_claims_anchors_nothing() {
List existing = [System("nas01", "192.0.2.50")];
List incoming = [Service("immich", "198.51.100.99", "NAS01.lan")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal(["NAS01.lan"], incoming.OfType().Single().RunsOn);
}
[Fact]
public void Systems_are_not_anchored_by_address() {
// Two systems sharing an address are the same machine or a conflict — never a
// parent and a child. Only services are hosted.
List existing = [System("nas01", "192.0.2.50")];
List incoming = [System("scanned-host", "192.0.2.50", "rpk1:net:b")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Empty(incoming.OfType().Single().RunsOn);
}
// ---------------------------------------------------------------
// Unifying a sweep's find with the guest a hypervisor already described
// ---------------------------------------------------------------
private static SystemResource Scanned(string name, string ip, string? mac = null) {
var card = new SystemResource {
Kind = SystemResource.KindLabel,
Name = name,
Ip = ip,
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, mac ?? $"ip:{ip}")
};
if (mac != null)
card.Labels["mac"] = mac;
return card;
}
private static SystemResource Guest(string name, string ip, string id = "abc123") =>
new() {
Kind = SystemResource.KindLabel,
Name = name,
Ip = ip,
DiscoveryId = $"rpk1:pve:{id}"
};
[Fact]
public void A_sweep_find_becomes_the_guest_the_hypervisor_already_described() {
// ARP is link-local, so a guest on another subnet gives the sweep no MAC and its
// identity falls back to the address. Now that the hypervisor reports that same
// address, it is the only thing tying the two records together.
List existing = [Guest("app-vm", "192.0.2.105")];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
SystemResource card = incoming.OfType().Single();
Assert.Equal("app-vm", card.Name);
// The sweep's weaker identity is dropped so the merge cannot downgrade the
// hypervisor's.
Assert.Null(card.DiscoveryId);
}
[Fact]
public void A_sweep_find_that_saw_a_mac_is_left_to_the_mac_rule() {
// A MAC is better evidence than an address. If it did not unify above, the two
// records disagree, and an address must not override that.
List existing = [Guest("app-vm", "192.0.2.105")];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105", "bc:24:11:00:1a:01")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal("host-1a2b3c4d", incoming.OfType().Single().Name);
}
[Fact]
public void Two_stored_systems_at_one_address_unify_nothing() {
// Overlapping subnets across sites, or a stale card. Ambiguity is not evidence.
List existing = [
Guest("site-a-vm", "192.0.2.105", "aaa111"),
Guest("site-b-vm", "192.0.2.105", "bbb222")
];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal("host-1a2b3c4d", incoming.OfType().Single().Name);
}
[Fact]
public void Two_sweep_finds_with_nothing_between_them_are_the_same_card_already() {
// Neither card is more than "something replied at this address", and an address
// is exactly what seeds their identity when no MAC was seen — so they carry the
// same id and the address rule never gets a say. The stored name wins, as it
// does for any re-run.
List existing = [Scanned("host-99999999", "192.0.2.105")];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
Assert.Equal(existing[0].DiscoveryId, incoming[0].DiscoveryId);
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal("host-99999999", incoming.OfType().Single().Name);
}
[Fact]
public void A_sweep_find_folds_into_one_that_saw_the_mac() {
// Not two stand-ins: the stored card names the NIC answering at that address —
// a firewall's neighbour table does this for every subnet it routes — where the
// incoming one only knows something replied. The MAC is the better identity, so
// the address-only card folds into it rather than becoming a second machine.
List existing = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
SystemResource card = incoming.OfType().Single();
Assert.Equal("host-99999999", card.Name);
// The weaker address-seeded identity is dropped so the merge cannot downgrade
// the MAC-seeded one it is landing on.
Assert.Null(card.DiscoveryId);
}
[Fact]
public void The_mac_wins_arriving_second_too() {
// Sweep the routed subnet first, ask the firewall after: same two facts, same
// one card. Here the MAC-seeded identity is the one that survives.
List existing = [Scanned("host-1a2b3c4d", "192.0.2.105")];
List incoming = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
SystemResource card = incoming.OfType().Single();
Assert.Equal("host-1a2b3c4d", card.Name);
Assert.NotNull(card.DiscoveryId);
}
[Fact]
public void Two_sweep_finds_that_each_saw_a_mac_never_unify() {
// Both name a NIC, and they name different ones. The MAC bridge has already had
// its say; sharing an address now is a conflict or an overlapping subnet.
List existing = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
List incoming = [Scanned("host-1a2b3c4d", "192.0.2.105", "bc:24:11:00:1a:0a")];
DiscoveryIdResolver.ResolveNames(existing, incoming);
Assert.Equal("host-1a2b3c4d", incoming.OfType().Single().Name);
}
}