BuildPhysicalTopologyUseCaseTests.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using NSubstitute;
  2. using RackPeek.Domain.Graph;
  3. using RackPeek.Domain.Graph.UseCases;
  4. using RackPeek.Domain.Persistence;
  5. using RackPeek.Domain.Resources.Connections;
  6. using RackPeek.Domain.Resources.Firewalls;
  7. using RackPeek.Domain.Resources.Hardware;
  8. using RackPeek.Domain.Resources.Servers;
  9. using RackPeek.Domain.Resources.SubResources;
  10. using RackPeek.Domain.Resources.Switches;
  11. namespace Tests.Graph;
  12. public sealed class BuildPhysicalTopologyUseCaseTests {
  13. private readonly IResourceCollection _repo = Substitute.For<IResourceCollection>();
  14. private readonly BuildPhysicalTopologyUseCase _useCase;
  15. public BuildPhysicalTopologyUseCaseTests() {
  16. _useCase = new BuildPhysicalTopologyUseCase(_repo);
  17. }
  18. private void Seed(IReadOnlyList<Hardware> hardware, params Connection[] connections) {
  19. _repo.HardwareResources.Returns(hardware);
  20. _repo.GetConnectionsAsync().Returns(connections);
  21. }
  22. private static Server Server(string name) => new() { Name = name, Kind = "Server" };
  23. private static Switch Switch(string name, params Port[] ports) =>
  24. new() { Name = name, Kind = "Switch", Ports = ports.ToList() };
  25. private static Firewall Firewall(string name, params Port[] ports) =>
  26. new() { Name = name, Kind = "Firewall", Ports = ports.ToList() };
  27. [Fact]
  28. public async Task Empty_Inventory_Produces_Empty_Graph() {
  29. Seed([]);
  30. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  31. Assert.Empty(graph.Nodes);
  32. Assert.Empty(graph.Edges);
  33. }
  34. [Fact]
  35. public async Task Each_Hardware_Resource_Becomes_A_Node() {
  36. Seed([Server("srv-01"), Switch("sw-01"), Firewall("fw-01")]);
  37. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  38. Assert.Equal(3, graph.Nodes.Count);
  39. Assert.Contains(graph.Nodes, n => n.Id == "srv-01" && n.Kind == "Server");
  40. Assert.Contains(graph.Nodes, n => n.Id == "sw-01" && n.Kind == "Switch");
  41. Assert.Contains(graph.Nodes, n => n.Id == "fw-01" && n.Kind == "Firewall");
  42. }
  43. [Fact]
  44. public async Task Nodes_Are_Sorted_For_Deterministic_Output() {
  45. Seed([Server("srv-02"), Server("srv-01"), Switch("sw-01")]);
  46. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  47. // Sorted by kind then name for a stable diagram across runs.
  48. Assert.Equal(new[] { "Server", "Server", "Switch" },
  49. graph.Nodes.Select(n => n.Kind).ToArray());
  50. Assert.Equal(new[] { "srv-01", "srv-02", "sw-01" },
  51. graph.Nodes.Select(n => n.Id).ToArray());
  52. }
  53. [Fact]
  54. public async Task Connection_Between_Two_Hardware_Resources_Becomes_An_Edge() {
  55. Seed(
  56. [Server("srv-01"), Switch("sw-01")],
  57. new Connection {
  58. A = new PortReference { Resource = "srv-01", PortGroup = 0, PortIndex = 0 },
  59. B = new PortReference { Resource = "sw-01", PortGroup = 0, PortIndex = 1 }
  60. });
  61. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  62. GraphEdge edge = Assert.Single(graph.Edges);
  63. Assert.Equal("srv-01", edge.Source);
  64. Assert.Equal("sw-01", edge.Target);
  65. Assert.Equal("connection", edge.Kind);
  66. }
  67. [Fact]
  68. public async Task Edge_Label_Uses_Explicit_Connection_Label_When_Present() {
  69. Seed(
  70. [Server("srv-01"), Switch("sw-01")],
  71. new Connection {
  72. Label = "primary uplink",
  73. A = new PortReference { Resource = "srv-01", PortGroup = 0, PortIndex = 0 },
  74. B = new PortReference { Resource = "sw-01", PortGroup = 0, PortIndex = 1 }
  75. });
  76. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  77. Assert.Equal("primary uplink", graph.Edges[0].Label);
  78. }
  79. [Fact]
  80. public async Task Edge_Label_Derived_From_Port_Group_Types_When_No_Explicit_Label() {
  81. Seed(
  82. [
  83. Switch("sw-01", new Port { Type = "RJ45", Count = 24 }),
  84. Firewall("fw-01", new Port { Type = "SFP+", Count = 4 })
  85. ],
  86. new Connection {
  87. A = new PortReference { Resource = "sw-01", PortGroup = 0, PortIndex = 3 },
  88. B = new PortReference { Resource = "fw-01", PortGroup = 0, PortIndex = 1 }
  89. });
  90. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  91. Assert.Equal("RJ453 ↔ SFP+1", graph.Edges[0].Label);
  92. }
  93. [Fact]
  94. public async Task Connection_Referencing_Unknown_Resource_Is_Dropped() {
  95. // Defensive: stale connection pointing at a deleted resource shouldn't
  96. // crash the diagram or produce a dangling edge.
  97. Seed(
  98. [Server("srv-01")],
  99. new Connection {
  100. A = new PortReference { Resource = "srv-01", PortGroup = 0, PortIndex = 0 },
  101. B = new PortReference { Resource = "deleted-host", PortGroup = 0, PortIndex = 0 }
  102. });
  103. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  104. Assert.Empty(graph.Edges);
  105. }
  106. [Fact]
  107. public async Task Tags_Are_Carried_Onto_The_Node_For_Future_Filtering() {
  108. Server server = Server("srv-01");
  109. server.Tags = ["homelab", "prod"];
  110. Seed([server]);
  111. RackPeek.Domain.Graph.Graph graph = await _useCase.ExecuteAsync();
  112. Assert.Equal("homelab,prod", graph.Nodes[0].Data!["tags"]);
  113. }
  114. }