4
0

InventoryReadEndpointTests.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. using System.Net;
  2. using System.Net.Http.Json;
  3. using System.Text.Json;
  4. using RackPeek.Domain.Api;
  5. using Xunit.Abstractions;
  6. namespace Tests.Api;
  7. [Collection("Process-wide static state")]
  8. public class InventoryReadEndpointTests(ITestOutputHelper output) : ApiTestBase(output) {
  9. private const string _seedYaml = """
  10. resources:
  11. - kind: Server
  12. name: srv-read
  13. tags:
  14. - homelab
  15. ports:
  16. - type: rj45
  17. speed: 1
  18. count: 2
  19. - kind: Switch
  20. name: sw-read
  21. ports:
  22. - type: rj45
  23. speed: 1
  24. count: 4
  25. - kind: AccessPoint
  26. name: ap-read
  27. model: U6 Lite
  28. - kind: System
  29. name: pve-read
  30. runsOn:
  31. - srv-read
  32. type: hypervisor
  33. os: Proxmox VE 9
  34. - kind: Service
  35. name: svc-read
  36. runsOn:
  37. - pve-read
  38. network:
  39. ip: 10.0.0.5
  40. port: 8080
  41. connections:
  42. - a:
  43. resource: srv-read
  44. portGroup: 0
  45. portIndex: 0
  46. b:
  47. resource: sw-read
  48. portGroup: 0
  49. portIndex: 0
  50. """;
  51. private async Task SeedAsync(HttpClient client) {
  52. HttpResponseMessage seed = await client.PostAsJsonAsync("/api/inventory", new { yaml = _seedYaml });
  53. Assert.True(seed.StatusCode == HttpStatusCode.OK,
  54. $"seed failed: {await seed.Content.ReadAsStringAsync()}");
  55. }
  56. [Fact]
  57. public async Task Get_Inventory_Without_Api_Key_Returns_401() {
  58. HttpClient client = CreateClient();
  59. HttpResponseMessage response = await client.GetAsync("/api/inventory");
  60. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  61. }
  62. [Fact]
  63. public async Task Get_Summary_Without_Api_Key_Returns_401() {
  64. HttpClient client = CreateClient();
  65. HttpResponseMessage response = await client.GetAsync("/api/inventory/summary");
  66. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  67. }
  68. [Fact]
  69. public async Task Get_Inventory_Returns_Full_Document() {
  70. HttpClient client = CreateClient(true);
  71. await SeedAsync(client);
  72. HttpResponseMessage response = await client.GetAsync("/api/inventory");
  73. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  74. using JsonDocument doc = await ReadJsonAsync(response);
  75. JsonElement root = doc.RootElement;
  76. Assert.Equal(RackPeek.Domain.Persistence.Yaml.RackPeekConfigMigrationDeserializer.ListOfMigrations.Count,
  77. root.GetProperty("version").GetInt32());
  78. Assert.Equal(5, root.GetProperty("resources").GetArrayLength());
  79. Assert.Equal(1, root.GetProperty("connections").GetArrayLength());
  80. JsonElement server = root.GetProperty("resources").EnumerateArray()
  81. .First(r => r.GetProperty("name").GetString() == "srv-read");
  82. Assert.Equal("Server", server.GetProperty("kind").GetString());
  83. Assert.Contains("homelab", server.GetProperty("tags").EnumerateArray()
  84. .Select(t => t.GetString()));
  85. // camelCase field names, matching the published schema
  86. Assert.Equal("rj45", server.GetProperty("ports")[0].GetProperty("type").GetString());
  87. JsonElement connection = root.GetProperty("connections")[0];
  88. Assert.Equal("srv-read", connection.GetProperty("a").GetProperty("resource").GetString());
  89. Assert.Equal("sw-read", connection.GetProperty("b").GetProperty("resource").GetString());
  90. }
  91. [Fact]
  92. public async Task Get_Inventory_Emits_Json_Discriminator_Casing() {
  93. // The Resource.Kind property and the URL/routing kind say "Accesspoint";
  94. // the JSON discriminator the import endpoint requires is "AccessPoint".
  95. // The document must always carry the importable spelling.
  96. HttpClient client = CreateClient(true);
  97. await SeedAsync(client);
  98. using JsonDocument doc = await ReadJsonAsync(await client.GetAsync("/api/inventory"));
  99. JsonElement ap = doc.RootElement.GetProperty("resources").EnumerateArray()
  100. .First(r => r.GetProperty("name").GetString() == "ap-read");
  101. Assert.Equal("AccessPoint", ap.GetProperty("kind").GetString());
  102. }
  103. [Fact]
  104. public async Task Get_Inventory_Document_Round_Trips_Through_Import_As_No_Op() {
  105. // The whole point of the read API for a companion app: fetch the document,
  106. // send it back unchanged, and the server reports nothing changed.
  107. HttpClient client = CreateClient(true);
  108. await SeedAsync(client);
  109. using JsonDocument doc = await ReadJsonAsync(await client.GetAsync("/api/inventory"));
  110. JsonElement document = JsonDocument.Parse(doc.RootElement.GetRawText()).RootElement;
  111. HttpResponseMessage response = await client.PostAsJsonAsync("/api/inventory",
  112. new {
  113. json = document,
  114. dryRun = true
  115. });
  116. Assert.True(response.StatusCode == HttpStatusCode.OK,
  117. $"round trip failed: {await response.Content.ReadAsStringAsync()}");
  118. ImportYamlResponse? result = await response.Content.ReadFromJsonAsync<ImportYamlResponse>();
  119. Assert.Empty(result!.Added);
  120. Assert.Empty(result.Updated);
  121. Assert.Empty(result.Replaced);
  122. Assert.Empty(result.ConnectionsAdded);
  123. Assert.Empty(result.ConnectionsRemoved);
  124. }
  125. [Fact]
  126. public async Task Get_Summary_Returns_Counts() {
  127. HttpClient client = CreateClient(true);
  128. await SeedAsync(client);
  129. using JsonDocument doc = await ReadJsonAsync(await client.GetAsync("/api/inventory/summary"));
  130. JsonElement root = doc.RootElement;
  131. Assert.Equal(3, root.GetProperty("hardware").GetProperty("totalHardware").GetInt32());
  132. Assert.Equal(1, root.GetProperty("systems").GetProperty("totalSystems").GetInt32());
  133. Assert.Equal(1, root.GetProperty("services").GetProperty("totalServices").GetInt32());
  134. Assert.Equal(1, root.GetProperty("tags").GetProperty("homelab").GetInt32());
  135. }
  136. [Fact]
  137. public async Task Get_Search_Returns_Ranked_Matches() {
  138. HttpClient client = CreateClient(true);
  139. await SeedAsync(client);
  140. using JsonDocument doc =
  141. await ReadJsonAsync(await client.GetAsync("/api/inventory/search?q=svc-read"));
  142. JsonElement matches = doc.RootElement.GetProperty("matches");
  143. Assert.True(matches.GetArrayLength() >= 1);
  144. Assert.Equal("svc-read", matches[0].GetProperty("name").GetString());
  145. Assert.Equal("Service", matches[0].GetProperty("kind").GetString());
  146. }
  147. [Fact]
  148. public async Task Get_Search_With_Missing_Query_Returns_400() {
  149. HttpClient client = CreateClient(true);
  150. HttpResponseMessage response = await client.GetAsync("/api/inventory/search");
  151. Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  152. }
  153. [Fact]
  154. public async Task Get_Tree_Returns_Containment_Forest() {
  155. HttpClient client = CreateClient(true);
  156. await SeedAsync(client);
  157. using JsonDocument doc = await ReadJsonAsync(await client.GetAsync("/api/inventory/tree"));
  158. JsonElement hardware = doc.RootElement.GetProperty("hardware");
  159. JsonElement server = hardware.EnumerateArray()
  160. .First(h => h.GetProperty("hardwareName").GetString() == "srv-read");
  161. Assert.Equal("Server", server.GetProperty("kind").GetString());
  162. Assert.Single(server.GetProperty("systems").EnumerateArray());
  163. Assert.Contains("svc-read",
  164. server.GetProperty("systems")[0].GetProperty("services").EnumerateArray()
  165. .Select(s => s.GetString()));
  166. }
  167. private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response) {
  168. var body = await response.Content.ReadAsStringAsync();
  169. return JsonDocument.Parse(body);
  170. }
  171. }