diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs index a8c64e7..f08bff3 100644 --- a/src/PSProxmoxVE.Core/Services/NodeService.cs +++ b/src/PSProxmoxVE.Core/Services/NodeService.cs @@ -38,8 +38,9 @@ namespace PSProxmoxVE.Core.Services return Invoke(session, client => { var response = client.GetAsync("nodes").GetAwaiter().GetResult(); - var data = JObject.Parse(response)["data"]; - return data?.ToObject() ?? Array.Empty(); + var data = JObject.Parse(response)["data"] + ?? throw new InvalidOperationException("Response did not contain a 'data' field."); + return data.ToObject() ?? Array.Empty(); }); } @@ -54,8 +55,14 @@ namespace PSProxmoxVE.Core.Services return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/status").GetAwaiter().GetResult(); - var data = JObject.Parse(response)["data"]; - return data?.ToObject() ?? new PveNodeStatus(); + var data = JObject.Parse(response)["data"] + ?? throw new InvalidOperationException("Response did not contain a 'data' field."); + var status = data.ToObject() + ?? throw new InvalidOperationException("Failed to deserialize node status."); + // The /nodes/{node}/status response does not include the node name. + if (string.IsNullOrEmpty(status.Node)) + status.Node = node; + return status; }); } diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeCmdlet.cs index cd4bbf7..5953116 100644 --- a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeCmdlet.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Management.Automation; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Nodes; +using PSProxmoxVE.Core.Services; namespace PSProxmoxVE.Cmdlets.Nodes { @@ -30,27 +27,8 @@ namespace PSProxmoxVE.Cmdlets.Nodes var session = GetSession(); WriteVerbose("Getting cluster nodes..."); - string responseBody; - try - { - using var client = new PveHttpClient(session); - responseBody = client.GetAsync("nodes").GetAwaiter().GetResult(); - } - catch (Exception ex) - { - ThrowTerminatingError(new ErrorRecord( - ex, - "GetPveNodeFailed", - ErrorCategory.ConnectionError, - session.Hostname)); - return; - } - - var json = JObject.Parse(responseBody); - var dataToken = json["data"] ?? throw new InvalidOperationException("Response did not contain a 'data' field."); - - var nodes = dataToken.ToObject>( - JsonSerializer.CreateDefault()) ?? new List(); + var service = new NodeService(); + var nodes = service.GetNodes(session); foreach (var node in nodes) { diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeStatusCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeStatusCmdlet.cs index 7215fe1..7fc73d9 100644 --- a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeStatusCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeStatusCmdlet.cs @@ -1,9 +1,6 @@ -using System; using System.Management.Automation; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Nodes; +using PSProxmoxVE.Core.Services; namespace PSProxmoxVE.Cmdlets.Nodes { @@ -32,33 +29,8 @@ namespace PSProxmoxVE.Cmdlets.Nodes var session = GetSession(); WriteVerbose($"Getting status for node '{Node}'..."); - var resource = $"nodes/{Uri.EscapeDataString(Node)}/status"; - - string responseBody; - try - { - using var client = new PveHttpClient(session); - responseBody = client.GetAsync(resource).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - ThrowTerminatingError(new ErrorRecord( - ex, - "GetPveNodeStatusFailed", - ErrorCategory.ConnectionError, - Node)); - return; - } - - var json = JObject.Parse(responseBody); - var dataToken = json["data"] ?? throw new InvalidOperationException("Response did not contain a 'data' field."); - - var status = dataToken.ToObject(JsonSerializer.CreateDefault()) - ?? throw new InvalidOperationException("Failed to deserialize node status."); - - // The /nodes/{node}/status response does not include the node name; populate it. - if (string.IsNullOrEmpty(status.Node)) - status.Node = Node; + var service = new NodeService(); + var status = service.GetNodeStatus(session, Node); WriteObject(status); } diff --git a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs index f0e14a3..4b01c07 100644 --- a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs +++ b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs @@ -44,6 +44,18 @@ namespace PSProxmoxVE.Core.Tests.Services mockClient.Verify(c => c.GetAsync("nodes"), Times.Once); } + [Fact] + public void GetNodes_MissingDataField_Throws() + { + // Arrange + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync("nodes")).ReturnsAsync("{}"); + var service = new NodeService(mockClient.Object); + + // Act & Assert + Assert.Throws(() => service.GetNodes(CreateSession())); + } + [Fact] public void GetNodeStatus_ReturnsPveNodeStatus() { @@ -70,6 +82,61 @@ namespace PSProxmoxVE.Core.Tests.Services mockClient.Verify(c => c.GetAsync("nodes/pve1/status"), Times.Once); } + [Fact] + public void GetNodeStatus_StampsNodeWhenResponseOmitsIt() + { + // Arrange + var json = @"{""data"": {""status"": ""online"", ""maxcpu"": 16}}"; + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync("nodes/pve1/status")).ReturnsAsync(json); + var service = new NodeService(mockClient.Object); + + // Act + var status = service.GetNodeStatus(CreateSession(), "pve1"); + + // Assert + Assert.Equal("pve1", status.Node); + mockClient.Verify(c => c.GetAsync("nodes/pve1/status"), Times.Once); + } + + [Fact] + public void GetNodeStatus_EscapesNodeInPath() + { + // Arrange + var json = @"{""data"": {""node"": ""pve node"", ""status"": ""online""}}"; + string? capturedPath = null; + var calls = 0; + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync(It.IsAny())) + .Callback(path => + { + capturedPath = path; + calls++; + }) + .ReturnsAsync(json); + var service = new NodeService(mockClient.Object); + + // Act + var status = service.GetNodeStatus(CreateSession(), "pve node"); + + // Assert + Assert.Equal("nodes/pve%20node/status", capturedPath); + Assert.Equal(1, calls); + Assert.Equal("pve node", status.Node); + } + + [Fact] + public void GetNodeStatus_MissingDataField_Throws() + { + // Arrange + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync("nodes/pve1/status")).ReturnsAsync("{}"); + var service = new NodeService(mockClient.Object); + + // Act & Assert + Assert.Throws(() => service.GetNodeStatus(CreateSession(), "pve1")); + } + [Fact] public void GetNodeConfig_ReturnsDictionary() {