using RackPeek.Domain.Api;
using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// Network-scan output through the real server: pushed over HTTP, merged by the
/// real resolver, asserted against what lands on disk. These pin the identity
/// contracts a scan lives or dies by — idempotent re-runs, renames that stick,
/// and never stealing the identity of a host another collector documented.
///
public class NetworkDiscoveryMergeTests {
private static string ScanYaml(params NetworkHostFact[] hosts) =>
DiscoveryDocument.ToYaml(NetworkScanMapper.ToResources(hosts));
private static NetworkHostFact Nas(string ip = "192.168.1.20") =>
new(ip, "dc:a6:32:0f:11:22", "nas01.lan", true, []);
[Fact]
public async Task A_scan_lands_on_disk_and_a_rescan_changes_nothing() {
using var api = new DiscoveryApiFixture();
ImportYamlResponse first = await api.PublishAsync(ScanYaml(Nas()));
Assert.Equal(["nas01"], first.Added);
Assert.Contains("discoveryId: rpk1:net:", api.StoredYaml);
Assert.Contains("mac: dc:a6:32:0f:11:22", api.StoredYaml);
Fixture.AssertConformsToSchema(api.StoredYaml);
ImportYamlResponse second = await api.PublishAsync(ScanYaml(Nas()));
Assert.Empty(second.Added);
Assert.Empty(second.Updated);
}
[Fact]
public async Task A_users_rename_survives_the_next_scan() {
// The stored card is a previous scan of the same machine that the user has
// since renamed — the id stayed with it, as the UI keeps it on a rename.
List renamed = NetworkScanMapper.ToResources([Nas()]);
renamed[0].Name = "storage-primary";
using var api = new DiscoveryApiFixture(DiscoveryDocument.ToYaml(renamed));
ImportYamlResponse rescan = await api.PublishAsync(ScanYaml(Nas()));
Assert.Empty(rescan.Added);
Assert.Contains("storage-primary", api.StoredYaml);
Assert.DoesNotContain("name: nas01", api.StoredYaml);
}
[Fact]
public async Task A_dhcp_move_updates_the_address_of_the_same_machine() {
using var api = new DiscoveryApiFixture();
await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.20")));
ImportYamlResponse moved = await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.99")));
Assert.Empty(moved.Added); // same MAC, same machine
Assert.Equal(["nas01"], moved.Updated);
Assert.Contains("ip: 192.168.1.99", api.StoredYaml);
Assert.DoesNotContain("192.168.1.20", api.StoredYaml);
}
[Fact]
public async Task A_scan_never_steals_the_identity_of_an_agent_discovered_host() {
// nas01 exists with a machine-id identity but WITHOUT the macs label an agent
// records (an older agent, or a hand-stripped label) — so there is no MAC
// bridge, and the resolver must keep the cards apart rather than guess.
var agentDiscovered = DiscoveryDocument.ToYaml([
new SystemResource {
Kind = SystemResource.KindLabel,
Name = "nas01",
DiscoveryId = DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"),
Type = "baremetal",
Os = "Debian",
Cores = 12
}
]);
using var api = new DiscoveryApiFixture(agentDiscovered);
ImportYamlResponse response = await api.PublishAsync(ScanYaml(Nas()));
var scanName = Assert.Single(response.Added);
Assert.StartsWith("nas01-", scanName); // suffixed, not adopted
var stored = api.StoredYaml;
Assert.Contains("rpk1:sys:", stored); // the agent identity is intact
Assert.Contains("rpk1:net:", stored); // and the scan's card exists beside it
Assert.Contains("os: Debian", stored); // nothing on the original was touched
}
[Fact]
public async Task An_agent_claims_a_scanned_card_and_every_collector_lands_on_it_after() {
// The unification headline, scan-first: the sweep found the box, then
// `rpk discover system` runs on it. Same MAC, so it is the same card — the
// agent's identity and detail land on the scan's card instead of duplicating.
using var api = new DiscoveryApiFixture();
await api.PublishAsync(ScanYaml(Nas()));
SystemResource agent = SystemResourceMapper.ToResource(new SystemFacts {
Hostname = "nas01.lan",
MachineId = "machine-a",
Os = "Debian 12",
Cores = 12,
RamGb = 64,
Type = "baremetal",
Ip = "192.168.1.20",
Macs = ["dc:a6:32:0f:11:22"]
});
ImportYamlResponse claim = await api.PublishAsync(DiscoveryDocument.ToYaml([agent]));
// Nothing added: the agent updated the scan's card (which keeps its name).
Assert.Empty(claim.Added);
Assert.Equal(["nas01"], claim.Updated);
var stored = api.StoredYaml;
Assert.Contains("rpk1:sys:", stored); // identity upgraded to the agent's
Assert.DoesNotContain("rpk1:net:", stored);
Assert.Contains("os: Debian 12", stored);
Assert.Contains("mac: dc:a6:32:0f:11:22", stored);
Assert.Contains("macs: dc:a6:32:0f:11:22", stored);
Fixture.AssertConformsToSchema(stored);
// ...and a rescan afterwards still lands on that same card via the MAC.
ImportYamlResponse rescan = await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.99")));
Assert.Empty(rescan.Added);
Assert.Contains("rpk1:sys:", api.StoredYaml); // never downgraded
Assert.Contains("ip: 192.168.1.99", api.StoredYaml); // but freshly addressed
}
[Fact]
public async Task A_scan_enriches_an_agent_discovered_card_instead_of_duplicating_it() {
// The reverse order: the agent documented the box first, then a sweep sees it.
SystemResource agent = SystemResourceMapper.ToResource(new SystemFacts {
Hostname = "nas01",
MachineId = "machine-a",
Os = "Debian 12",
Cores = 12,
RamGb = 64,
Type = "baremetal",
Macs = ["dc:a6:32:0f:11:22"]
});
using var api = new DiscoveryApiFixture(DiscoveryDocument.ToYaml([agent]));
ImportYamlResponse scan = await api.PublishAsync(ScanYaml(Nas()));
Assert.Empty(scan.Added);
Assert.Equal(["nas01"], scan.Updated);
var stored = api.StoredYaml;
Assert.Contains("rpk1:sys:", stored); // the agent identity is untouched
Assert.DoesNotContain("rpk1:net:", stored);
Assert.Contains("ip: 192.168.1.20", stored); // the scan contributed the address
Assert.Contains("os: Debian 12", stored);
Fixture.AssertConformsToSchema(stored);
}
[Fact]
public async Task A_scan_adopts_a_hand_written_system_of_the_same_name() {
// The inverse case: the user typed the card themselves, so it has no id yet.
// The scan stamps its identity onto it and enriches it instead of duplicating.
using var api = new DiscoveryApiFixture(
"""
version: 4
resources:
- kind: System
name: nas01
type: baremetal
os: Debian
""");
ImportYamlResponse response = await api.PublishAsync(ScanYaml(Nas()));
Assert.Empty(response.Added);
Assert.Equal(["nas01"], response.Updated);
var stored = api.StoredYaml;
Assert.Contains("rpk1:net:", stored);
Assert.Contains("ip: 192.168.1.20", stored);
// The scan card is sparse on purpose, so everything the user wrote survives.
Assert.Contains("os: Debian", stored);
Assert.Contains("type: baremetal", stored);
}
}