NetworkDiscoveryMergeTests.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. using RackPeek.Domain.Api;
  2. using RackPeek.Domain.Discovery;
  3. using RackPeek.Domain.Resources;
  4. using RackPeek.Domain.Resources.SystemResources;
  5. namespace Tests.Discovery;
  6. /// <summary>
  7. /// Network-scan output through the real server: pushed over HTTP, merged by the
  8. /// real resolver, asserted against what lands on disk. These pin the identity
  9. /// contracts a scan lives or dies by — idempotent re-runs, renames that stick,
  10. /// and never stealing the identity of a host another collector documented.
  11. /// </summary>
  12. public class NetworkDiscoveryMergeTests {
  13. private static string ScanYaml(params NetworkHostFact[] hosts) =>
  14. DiscoveryDocument.ToYaml(NetworkScanMapper.ToResources(hosts));
  15. private static NetworkHostFact Nas(string ip = "192.168.1.20") =>
  16. new(ip, "dc:a6:32:0f:11:22", "nas01.lan", true, []);
  17. [Fact]
  18. public async Task A_scan_lands_on_disk_and_a_rescan_changes_nothing() {
  19. using var api = new DiscoveryApiFixture();
  20. ImportYamlResponse first = await api.PublishAsync(ScanYaml(Nas()));
  21. Assert.Equal(["nas01"], first.Added);
  22. Assert.Contains("discoveryId: rpk1:net:", api.StoredYaml);
  23. Assert.Contains("mac: dc:a6:32:0f:11:22", api.StoredYaml);
  24. Fixture.AssertConformsToSchema(api.StoredYaml);
  25. ImportYamlResponse second = await api.PublishAsync(ScanYaml(Nas()));
  26. Assert.Empty(second.Added);
  27. Assert.Empty(second.Updated);
  28. }
  29. [Fact]
  30. public async Task A_users_rename_survives_the_next_scan() {
  31. // The stored card is a previous scan of the same machine that the user has
  32. // since renamed — the id stayed with it, as the UI keeps it on a rename.
  33. List<Resource> renamed = NetworkScanMapper.ToResources([Nas()]);
  34. renamed[0].Name = "storage-primary";
  35. using var api = new DiscoveryApiFixture(DiscoveryDocument.ToYaml(renamed));
  36. ImportYamlResponse rescan = await api.PublishAsync(ScanYaml(Nas()));
  37. Assert.Empty(rescan.Added);
  38. Assert.Contains("storage-primary", api.StoredYaml);
  39. Assert.DoesNotContain("name: nas01", api.StoredYaml);
  40. }
  41. [Fact]
  42. public async Task A_dhcp_move_updates_the_address_of_the_same_machine() {
  43. using var api = new DiscoveryApiFixture();
  44. await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.20")));
  45. ImportYamlResponse moved = await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.99")));
  46. Assert.Empty(moved.Added); // same MAC, same machine
  47. Assert.Equal(["nas01"], moved.Updated);
  48. Assert.Contains("ip: 192.168.1.99", api.StoredYaml);
  49. Assert.DoesNotContain("192.168.1.20", api.StoredYaml);
  50. }
  51. [Fact]
  52. public async Task A_scan_never_steals_the_identity_of_an_agent_discovered_host() {
  53. // nas01 exists with a machine-id identity but WITHOUT the macs label an agent
  54. // records (an older agent, or a hand-stripped label) — so there is no MAC
  55. // bridge, and the resolver must keep the cards apart rather than guess.
  56. var agentDiscovered = DiscoveryDocument.ToYaml([
  57. new SystemResource {
  58. Kind = SystemResource.KindLabel,
  59. Name = "nas01",
  60. DiscoveryId = DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"),
  61. Type = "baremetal",
  62. Os = "Debian",
  63. Cores = 12
  64. }
  65. ]);
  66. using var api = new DiscoveryApiFixture(agentDiscovered);
  67. ImportYamlResponse response = await api.PublishAsync(ScanYaml(Nas()));
  68. var scanName = Assert.Single(response.Added);
  69. Assert.StartsWith("nas01-", scanName); // suffixed, not adopted
  70. var stored = api.StoredYaml;
  71. Assert.Contains("rpk1:sys:", stored); // the agent identity is intact
  72. Assert.Contains("rpk1:net:", stored); // and the scan's card exists beside it
  73. Assert.Contains("os: Debian", stored); // nothing on the original was touched
  74. }
  75. [Fact]
  76. public async Task An_agent_claims_a_scanned_card_and_every_collector_lands_on_it_after() {
  77. // The unification headline, scan-first: the sweep found the box, then
  78. // `rpk discover system` runs on it. Same MAC, so it is the same card — the
  79. // agent's identity and detail land on the scan's card instead of duplicating.
  80. using var api = new DiscoveryApiFixture();
  81. await api.PublishAsync(ScanYaml(Nas()));
  82. SystemResource agent = SystemResourceMapper.ToResource(new SystemFacts {
  83. Hostname = "nas01.lan",
  84. MachineId = "machine-a",
  85. Os = "Debian 12",
  86. Cores = 12,
  87. RamGb = 64,
  88. Type = "baremetal",
  89. Ip = "192.168.1.20",
  90. Macs = ["dc:a6:32:0f:11:22"]
  91. });
  92. ImportYamlResponse claim = await api.PublishAsync(DiscoveryDocument.ToYaml([agent]));
  93. // Nothing added: the agent updated the scan's card (which keeps its name).
  94. Assert.Empty(claim.Added);
  95. Assert.Equal(["nas01"], claim.Updated);
  96. var stored = api.StoredYaml;
  97. Assert.Contains("rpk1:sys:", stored); // identity upgraded to the agent's
  98. Assert.DoesNotContain("rpk1:net:", stored);
  99. Assert.Contains("os: Debian 12", stored);
  100. Assert.Contains("mac: dc:a6:32:0f:11:22", stored);
  101. Assert.Contains("macs: dc:a6:32:0f:11:22", stored);
  102. Fixture.AssertConformsToSchema(stored);
  103. // ...and a rescan afterwards still lands on that same card via the MAC.
  104. ImportYamlResponse rescan = await api.PublishAsync(ScanYaml(Nas(ip: "192.168.1.99")));
  105. Assert.Empty(rescan.Added);
  106. Assert.Contains("rpk1:sys:", api.StoredYaml); // never downgraded
  107. Assert.Contains("ip: 192.168.1.99", api.StoredYaml); // but freshly addressed
  108. }
  109. [Fact]
  110. public async Task A_scan_enriches_an_agent_discovered_card_instead_of_duplicating_it() {
  111. // The reverse order: the agent documented the box first, then a sweep sees it.
  112. SystemResource agent = SystemResourceMapper.ToResource(new SystemFacts {
  113. Hostname = "nas01",
  114. MachineId = "machine-a",
  115. Os = "Debian 12",
  116. Cores = 12,
  117. RamGb = 64,
  118. Type = "baremetal",
  119. Macs = ["dc:a6:32:0f:11:22"]
  120. });
  121. using var api = new DiscoveryApiFixture(DiscoveryDocument.ToYaml([agent]));
  122. ImportYamlResponse scan = await api.PublishAsync(ScanYaml(Nas()));
  123. Assert.Empty(scan.Added);
  124. Assert.Equal(["nas01"], scan.Updated);
  125. var stored = api.StoredYaml;
  126. Assert.Contains("rpk1:sys:", stored); // the agent identity is untouched
  127. Assert.DoesNotContain("rpk1:net:", stored);
  128. Assert.Contains("ip: 192.168.1.20", stored); // the scan contributed the address
  129. Assert.Contains("os: Debian 12", stored);
  130. Fixture.AssertConformsToSchema(stored);
  131. }
  132. [Fact]
  133. public async Task A_scan_adopts_a_hand_written_system_of_the_same_name() {
  134. // The inverse case: the user typed the card themselves, so it has no id yet.
  135. // The scan stamps its identity onto it and enriches it instead of duplicating.
  136. using var api = new DiscoveryApiFixture(
  137. """
  138. version: 4
  139. resources:
  140. - kind: System
  141. name: nas01
  142. type: baremetal
  143. os: Debian
  144. """);
  145. ImportYamlResponse response = await api.PublishAsync(ScanYaml(Nas()));
  146. Assert.Empty(response.Added);
  147. Assert.Equal(["nas01"], response.Updated);
  148. var stored = api.StoredYaml;
  149. Assert.Contains("rpk1:net:", stored);
  150. Assert.Contains("ip: 192.168.1.20", stored);
  151. // The scan card is sparse on purpose, so everything the user wrote survives.
  152. Assert.Contains("os: Debian", stored);
  153. Assert.Contains("type: baremetal", stored);
  154. }
  155. }