From 089bb7c81eb543c5dd730530f74e2887ed8bb31d Mon Sep 17 00:00:00 2001
From: "goodolclint-claude[bot]"
<323206664+goodolclint-claude[bot]@users.noreply.github.com>
Date: Thu, 3 Sep 2026 00:59:51 +0000
Subject: [PATCH] refactor: one task-response parser, and the dead code #154
names (part A) (#207)
* refactor: one task-response parser, and the dead code #154 names (part A)
Unifies the 8 byte-similar private ParseTask methods in BackupService,
ContainerService, NetworkService, NodeService, SnapshotService,
StorageService, TemplateService, and VmService into one shared
PveTaskResponse.Parse(json, node) utility, on the variant that stamps
Status = "running" for a bare-UPID response. BackupService, NodeService,
TemplateService, and VmService previously left Status null for that case;
their non--Wait task output now reports "running" like the other four
services already did.
Removes the duplicate ClusterConfigService.GetClusterStatus in favor of
ClusterService's; ClusterConfigService now holds a ClusterService built
from the same injected/default client, and WaitForQuorum and
Get-PveClusterStatus go through it.
Removes dead code named in issue #154 and its fold-in comments: the
never-called PveHttpClient/IPveHttpClient sync wrappers Put/Delete, the
never-thrown PveAuthenticationException, a #pragma around an
already-nullable field, the unreferenced TestHelper mock-handler helpers,
three hand-rolled version-warning blocks (now PveCmdletBase.WarnIfBelowVersion),
an unreachable catch(HttpRequestException) arm in WaitForStatusTransition,
dead ExitStatus-checking branches in ImportPveOvaCmdlet after WaitForTask
(which already throws on failure), and an unreachable int branch in
ApiValueHelper.IsExited.
Fixes the WaitForStatusTransition catch removal's premise: PveHttpClient
read the response body outside its HttpRequestException try block, so a
mid-body stream drop could still escape unwrapped. Moves the body read
inside the try so every HttpRequestException the client can throw becomes
a PveApiException, matching what the removed catch assumed.
Part of #154.
* Add service files: BackupService, ClusterConfigService, ContainerService, NetworkService
* Add service files: NodeService, SnapshotService, StorageService, TemplateService
* Add VmService and Utilities files
* Remove unused PveAuthenticationException (never thrown, caught, or tested)
* Add cmdlet files: GetPveClusterStatus, SDN subnets, PveCmdletBase, SendPveFile, ImportPveOva
* Add test files: BackupServiceTests, ClusterConfigServiceTests, NodeServiceTests
* Add remaining test files: TemplateServiceTests, VmServiceTests, TestHelper, ApiValueHelperTests, PveTaskResponseTests
* Add VmServiceTests
---------
Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
---
src/PSProxmoxVE.Core/Client/IPveHttpClient.cs | 6 ---
src/PSProxmoxVE.Core/Client/PveHttpClient.cs | 16 ++-----
.../Exceptions/PveAuthenticationException.cs | 23 ----------
.../Services/BackupService.cs | 17 +------
.../Services/ClusterConfigService.cs | 30 +++++--------
.../Services/ContainerService.cs | 36 ++++++---------
.../Services/NetworkService.cs | 18 +-------
src/PSProxmoxVE.Core/Services/NodeService.cs | 19 +-------
.../Services/SnapshotService.cs | 22 ++-------
.../Services/StorageService.cs | 22 ++-------
.../Services/TemplateService.cs | 18 +-------
src/PSProxmoxVE.Core/Services/VmService.cs | 31 +++++--------
.../Utilities/ApiValueHelper.cs | 8 ++--
.../Utilities/PveTaskResponse.cs | 36 +++++++++++++++
.../Cluster/GetPveClusterStatusCmdlet.cs | 2 +-
.../Cmdlets/Network/NewPveSdnSubnetCmdlet.cs | 8 +---
.../Cmdlets/Network/SetPveSdnSubnetCmdlet.cs | 8 +---
src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs | 38 ++++++++++++++--
.../Cmdlets/Storage/SendPveFileCmdlet.cs | 10 ++---
.../Cmdlets/Vms/ImportPveOvaCmdlet.cs | 22 +--------
.../Services/BackupServiceTests.cs | 1 +
.../Services/ClusterConfigServiceTests.cs | 39 ----------------
.../Services/NodeServiceTests.cs | 2 +
.../Services/TemplateServiceTests.cs | 1 +
.../Services/VmServiceTests.cs | 1 +
tests/PSProxmoxVE.Core.Tests/TestHelper.cs | 39 ----------------
.../Utilities/ApiValueHelperTests.cs | 3 +-
.../Utilities/PveTaskResponseTests.cs | 45 +++++++++++++++++++
28 files changed, 188 insertions(+), 333 deletions(-)
delete mode 100644 src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs
create mode 100644 src/PSProxmoxVE.Core/Utilities/PveTaskResponse.cs
create mode 100644 tests/PSProxmoxVE.Core.Tests/Utilities/PveTaskResponseTests.cs
diff --git a/src/PSProxmoxVE.Core/Client/IPveHttpClient.cs b/src/PSProxmoxVE.Core/Client/IPveHttpClient.cs
index 4783140..7e73d2c 100644
--- a/src/PSProxmoxVE.Core/Client/IPveHttpClient.cs
+++ b/src/PSProxmoxVE.Core/Client/IPveHttpClient.cs
@@ -35,12 +35,6 @@ namespace PSProxmoxVE.Core.Client
/// Synchronous wrapper for .
string Post(string resource, Dictionary? data = null);
- /// Synchronous wrapper for .
- string Put(string resource, Dictionary? data = null);
-
- /// Synchronous wrapper for .
- string Delete(string resource);
-
///
/// Uploads a file to a Proxmox VE storage endpoint using MultipartFormDataContent.
///
diff --git a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs
index ffc530f..2f8ad84 100644
--- a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs
+++ b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs
@@ -20,9 +20,7 @@ namespace PSProxmoxVE.Core.Client
///
public class PveHttpClient : IPveHttpClient
{
-#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type
private readonly PveSession? _session;
-#pragma warning restore CS8625
private readonly string _baseUrl;
private readonly HttpClient _httpClient;
private readonly HttpMessageHandler _handler;
@@ -266,14 +264,6 @@ namespace PSProxmoxVE.Core.Client
public string Post(string resource, Dictionary? data = null) =>
PostAsync(resource, data).GetAwaiter().GetResult();
- /// Synchronous wrapper for .
- public string Put(string resource, Dictionary? data = null) =>
- PutAsync(resource, data).GetAwaiter().GetResult();
-
- /// Synchronous wrapper for .
- public string Delete(string resource) =>
- DeleteAsync(resource).GetAwaiter().GetResult();
-
// -------------------------------------------------------------------------
// ISO / file upload
// -------------------------------------------------------------------------
@@ -434,9 +424,11 @@ namespace PSProxmoxVE.Core.Client
private async Task SendOnceAsync(HttpRequestMessage request, string resource, string httpMethod)
{
HttpResponseMessage response;
+ string body;
try
{
response = await _httpClient.SendAsync(request).ConfigureAwait(false);
+ body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
catch (TaskCanceledException ex)
{
@@ -452,12 +444,12 @@ namespace PSProxmoxVE.Core.Client
}
catch (HttpRequestException ex)
{
+ // Covers both a failed connection and a stream drop mid-body-read, so every
+ // HttpRequestException PveHttpClient can throw arrives as PveApiException.
throw new PveApiException(HttpStatusCode.ServiceUnavailable,
ex.Message, resource, httpMethod, ex);
}
- var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-
if (!response.IsSuccessStatusCode)
{
var errorMessage = ExtractErrorMessage(body, response.ReasonPhrase ?? response.StatusCode.ToString());
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs b/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs
deleted file mode 100644
index 083b20f..0000000
--- a/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-
-namespace PSProxmoxVE.Core.Exceptions
-{
- /// Exception thrown when authentication to a Proxmox VE server fails.
- public class PveAuthenticationException : Exception
- {
- /// Initializes a new instance with the specified error message.
- /// The error message describing the authentication failure.
- public PveAuthenticationException(string message)
- : base(message)
- {
- }
-
- /// Initializes a new instance with the specified error message and inner exception.
- /// The error message describing the authentication failure.
- /// The exception that caused this failure.
- public PveAuthenticationException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
- }
-}
diff --git a/src/PSProxmoxVE.Core/Services/BackupService.cs b/src/PSProxmoxVE.Core/Services/BackupService.cs
index 4c5ba9b..53010d6 100644
--- a/src/PSProxmoxVE.Core/Services/BackupService.cs
+++ b/src/PSProxmoxVE.Core/Services/BackupService.cs
@@ -47,7 +47,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -151,20 +151,5 @@ namespace PSProxmoxVE.Core.Services
return JsonHelper.ToListOfDictionaries(data as JArray);
});
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
index 560a9ae..1409f18 100644
--- a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
+++ b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
@@ -19,16 +19,24 @@ namespace PSProxmoxVE.Core.Services
private static readonly TimeSpan DefaultQuorumTimeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan QuorumPollInterval = TimeSpan.FromSeconds(2);
+ private readonly ClusterService _clusterService;
+
///
/// Initializes a new instance of the class.
///
- public ClusterConfigService() { }
+ public ClusterConfigService()
+ {
+ _clusterService = new ClusterService();
+ }
///
/// Initializes a new instance of the class with an injected HTTP client.
///
/// The HTTP client to use for API calls. The caller owns its lifetime.
- public ClusterConfigService(IPveHttpClient client) : base(client) { }
+ public ClusterConfigService(IPveHttpClient client) : base(client)
+ {
+ _clusterService = new ClusterService(client);
+ }
///
/// Returns the cluster configuration directory (GET /cluster/config).
@@ -297,22 +305,6 @@ namespace PSProxmoxVE.Core.Services
});
}
- ///
- /// Returns the current cluster status (GET /cluster/status).
- /// Delegates to the same endpoint as .
- ///
- public PveClusterStatus[] GetClusterStatus(PveSession session)
- {
- if (session == null) throw new ArgumentNullException(nameof(session));
-
- return Invoke(session, client =>
- {
- var response = client.GetAsync("cluster/status").GetAwaiter().GetResult();
- var data = JObject.Parse(response)["data"];
- return data?.ToObject() ?? Array.Empty();
- });
- }
-
///
/// Blocks until the cluster reports quorum (GET /cluster/status, quorate = 1).
///
@@ -335,7 +327,7 @@ namespace PSProxmoxVE.Core.Services
{
try
{
- foreach (var entry in GetClusterStatus(session))
+ foreach (var entry in _clusterService.GetClusterStatus(session))
{
if (string.Equals(entry.Type, "cluster", StringComparison.OrdinalIgnoreCase)
&& entry.Quorate == 1)
diff --git a/src/PSProxmoxVE.Core/Services/ContainerService.cs b/src/PSProxmoxVE.Core/Services/ContainerService.cs
index d6c8efb..557654a 100644
--- a/src/PSProxmoxVE.Core/Services/ContainerService.cs
+++ b/src/PSProxmoxVE.Core/Services/ContainerService.cs
@@ -6,6 +6,7 @@ using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Containers;
using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -201,7 +202,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -222,7 +223,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -243,7 +244,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -270,7 +271,7 @@ namespace PSProxmoxVE.Core.Services
kvp => kvp.Value?.ToString() ?? string.Empty);
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -300,7 +301,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/shutdown", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -325,7 +326,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{queryString}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -356,7 +357,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/clone", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -382,7 +383,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/migrate", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -422,7 +423,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/resize", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -447,7 +448,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/move_volume", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -467,7 +468,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/template")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -505,19 +506,8 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/{action}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/NetworkService.cs b/src/PSProxmoxVE.Core/Services/NetworkService.cs
index 321891b..5309310 100644
--- a/src/PSProxmoxVE.Core/Services/NetworkService.cs
+++ b/src/PSProxmoxVE.Core/Services/NetworkService.cs
@@ -6,6 +6,7 @@ using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -141,7 +142,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/network")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -588,20 +589,5 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
});
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs
index f08bff3..957a079 100644
--- a/src/PSProxmoxVE.Core/Services/NodeService.cs
+++ b/src/PSProxmoxVE.Core/Services/NodeService.cs
@@ -153,7 +153,7 @@ namespace PSProxmoxVE.Core.Services
return Invoke(session, client =>
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/startall", formData).GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -172,7 +172,7 @@ namespace PSProxmoxVE.Core.Services
return Invoke(session, client =>
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/stopall", formData).GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -193,20 +193,5 @@ namespace PSProxmoxVE.Core.Services
return PveVersion.Parse(versionStr!);
});
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/SnapshotService.cs b/src/PSProxmoxVE.Core/Services/SnapshotService.cs
index 0d180b0..4c2916b 100644
--- a/src/PSProxmoxVE.Core/Services/SnapshotService.cs
+++ b/src/PSProxmoxVE.Core/Services/SnapshotService.cs
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -78,7 +79,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -103,7 +104,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -128,23 +129,8 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/StorageService.cs b/src/PSProxmoxVE.Core/Services/StorageService.cs
index 1560f92..322ef56 100644
--- a/src/PSProxmoxVE.Core/Services/StorageService.cs
+++ b/src/PSProxmoxVE.Core/Services/StorageService.cs
@@ -6,6 +6,7 @@ using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Storage;
using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -137,7 +138,7 @@ namespace PSProxmoxVE.Core.Services
checksumAlgorithm,
progressCallback)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -182,7 +183,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/download-url", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -337,23 +338,8 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content", config)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/TemplateService.cs b/src/PSProxmoxVE.Core/Services/TemplateService.cs
index 569b830..cabb42f 100644
--- a/src/PSProxmoxVE.Core/Services/TemplateService.cs
+++ b/src/PSProxmoxVE.Core/Services/TemplateService.cs
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -68,7 +69,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/template")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -92,20 +93,5 @@ namespace PSProxmoxVE.Core.Services
return _vmService.RemoveVm(session, node, vmid, purge);
}
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs
index 1de2033..7562c17 100644
--- a/src/PSProxmoxVE.Core/Services/VmService.cs
+++ b/src/PSProxmoxVE.Core/Services/VmService.cs
@@ -249,7 +249,7 @@ namespace PSProxmoxVE.Core.Services
// POST (not PUT) because import-from triggers a background task
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -276,7 +276,7 @@ namespace PSProxmoxVE.Core.Services
kvp => kvp.Value?.ToString() ?? string.Empty);
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -312,7 +312,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/shutdown", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -343,7 +343,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/reboot", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -395,7 +395,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{queryString}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -435,7 +435,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/clone", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -468,7 +468,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/migrate", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -504,7 +504,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/resize", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
@@ -521,21 +521,10 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/{action}")
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
- private static PveTask ParseTask(string response, string node)
- {
- var data = JObject.Parse(response)["data"];
- // Many endpoints return the UPID string directly as the data value
- if (data?.Type == JTokenType.String)
- return new PveTask { Upid = data.ToString(), Node = node };
-
- var task = data?.ToObject() ?? new PveTask();
- task.Node = node;
- return task;
- }
// -------------------------------------------------------------------------
// QEMU Guest Agent
@@ -664,7 +653,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/move_disk", formData)
.GetAwaiter().GetResult();
- return ParseTask(response, node);
+ return PveTaskResponse.Parse(response, node);
});
}
diff --git a/src/PSProxmoxVE.Core/Utilities/ApiValueHelper.cs b/src/PSProxmoxVE.Core/Utilities/ApiValueHelper.cs
index 8296cae..b20a601 100644
--- a/src/PSProxmoxVE.Core/Utilities/ApiValueHelper.cs
+++ b/src/PSProxmoxVE.Core/Utilities/ApiValueHelper.cs
@@ -10,8 +10,9 @@ namespace PSProxmoxVE.Core.Utilities
{
///
/// Determines if a value represents a true/exited state.
- /// Accepts boolean true, integer 1 (as Int64 or Int32), and string "1" as true.
- /// All other values (false, 0, "0", null, etc.) are false.
+ /// Accepts boolean true, integer 1 (as Int64, which is how Newtonsoft deserializes a
+ /// JSON integer), and string "1" as true. All other values (false, 0, "0", null, etc.)
+ /// are false.
///
/// The value to check, typically from API response data.
/// True if the value represents an exited/true state, false otherwise.
@@ -26,9 +27,6 @@ namespace PSProxmoxVE.Core.Utilities
if (value is long l)
return l == 1L;
- if (value is int i)
- return i == 1;
-
if (value is string s)
return s == "1";
diff --git a/src/PSProxmoxVE.Core/Utilities/PveTaskResponse.cs b/src/PSProxmoxVE.Core/Utilities/PveTaskResponse.cs
new file mode 100644
index 0000000..28be3c1
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Utilities/PveTaskResponse.cs
@@ -0,0 +1,36 @@
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Core.Utilities
+{
+ ///
+ /// Parses the task returned by a mutating Proxmox VE API call. Most such endpoints return
+ /// the UPID as a bare string in data; a few return a task object directly.
+ ///
+ public static class PveTaskResponse
+ {
+ ///
+ /// Parses as a Proxmox VE API envelope and extracts the task
+ /// its data field describes, stamping with
+ /// .
+ ///
+ /// The raw JSON response body.
+ /// The cluster node the request was made against.
+ ///
+ /// A with and
+ /// set to "running" when data is a UPID
+ /// string; the deserialized task when data is an object; or an empty task
+ /// when data is null or absent.
+ ///
+ public static PveTask Parse(string json, string node)
+ {
+ var data = JObject.Parse(json)["data"];
+ if (data?.Type == JTokenType.String)
+ return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
+
+ var task = data?.ToObject() ?? new PveTask();
+ task.Node = node;
+ return task;
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs
index 50a8a45..c9dbb7a 100644
--- a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs
@@ -18,7 +18,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
protected override void ProcessRecord()
{
var session = GetSession();
- var service = new ClusterConfigService();
+ var service = new ClusterService();
WriteVerbose("Getting cluster status...");
var statuses = service.GetClusterStatus(session);
diff --git a/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnSubnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnSubnetCmdlet.cs
index 819b566..aa4bbd7 100644
--- a/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnSubnetCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnSubnetCmdlet.cs
@@ -48,12 +48,8 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
- if (!string.IsNullOrEmpty(DhcpRange)
- && session.ServerVersion != null && !session.ServerVersion.IsAtLeast(8, 1))
- {
- WriteWarning("The -DhcpRange parameter requires PVE 8.1 or later. "
- + $"Connected server is PVE {session.ServerVersion}. The parameter will be sent but may be ignored.");
- }
+ WarnIfBelowVersion(session, !string.IsNullOrEmpty(DhcpRange), 8, 1,
+ "The -DhcpRange parameter requires", "The parameter will be sent but may be ignored.");
WriteVerbose($"Creating SDN subnet '{Subnet}' on VNet '{Vnet}'...");
var data = new Dictionary
diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs
index e223eed..4e15119 100644
--- a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs
@@ -47,12 +47,8 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
- if (!string.IsNullOrEmpty(DhcpRange)
- && session.ServerVersion != null && !session.ServerVersion.IsAtLeast(8, 1))
- {
- WriteWarning("The -DhcpRange parameter requires PVE 8.1 or later. "
- + $"Connected server is PVE {session.ServerVersion}. The parameter will be sent but may be ignored.");
- }
+ WarnIfBelowVersion(session, !string.IsNullOrEmpty(DhcpRange), 8, 1,
+ "The -DhcpRange parameter requires", "The parameter will be sent but may be ignored.");
var service = new NetworkService();
diff --git a/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs b/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs
index c3bf6f8..7e2fd63 100644
--- a/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs
+++ b/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs
@@ -92,6 +92,40 @@ namespace PSProxmoxVE.Cmdlets
}
}
+ ///
+ /// Emits a soft warning when holds and the connected
+ /// server is below ..
+ /// Unlike , this never blocks the call: the parameter or
+ /// feature may simply be silently ignored by an older server.
+ ///
+ /// The authenticated PVE session.
+ /// True when the caller used the parameter/feature this warning covers.
+ /// Major version the parameter/feature requires.
+ /// Minor version the parameter/feature requires.
+ ///
+ /// The warning's leading clause, ending in "requires"/"require" (e.g. "The -DhcpRange
+ /// parameter requires"), so the full sentence reads
+ /// "<requirementClause> PVE <requiredMajor>.<requiredMinor> or later.".
+ ///
+ /// Trailing sentence describing what happens if the server is too old.
+ protected void WarnIfBelowVersion(
+ PveSession session,
+ bool condition,
+ int requiredMajor,
+ int requiredMinor,
+ string requirementClause,
+ string consequence)
+ {
+ if (!condition) return;
+
+ var version = session.ServerVersion;
+ if (version == null || version.IsAtLeast(requiredMajor, requiredMinor)) return;
+
+ WriteWarning(
+ $"{requirementClause} PVE {requiredMajor}.{requiredMinor} or later. " +
+ $"Connected server is PVE {version}. {consequence}");
+ }
+
///
/// Waits for a PVE task to complete, then optionally polls VM status until
/// it matches . Used by lifecycle cmdlets
@@ -151,10 +185,6 @@ namespace PSProxmoxVE.Cmdlets
{
WriteVerbose($"Status poll failed, retrying: {ex.Message}");
}
- catch (System.Net.Http.HttpRequestException ex)
- {
- WriteVerbose($"Status poll failed, retrying: {ex.Message}");
- }
System.Threading.Thread.Sleep(2000);
}
diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
index 4b7ac7b..b4173bd 100644
--- a/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
@@ -75,12 +75,10 @@ namespace PSProxmoxVE.Cmdlets.Storage
var session = GetSession();
- if ((!string.IsNullOrEmpty(Checksum) || !string.IsNullOrEmpty(ChecksumAlgorithm))
- && session.ServerVersion != null && !session.ServerVersion.IsAtLeast(7, 1))
- {
- WriteWarning("The -Checksum and -ChecksumAlgorithm parameters require PVE 7.1 or later. "
- + $"Connected server is PVE {session.ServerVersion}. The upload will proceed without checksum verification.");
- }
+ WarnIfBelowVersion(session,
+ !string.IsNullOrEmpty(Checksum) || !string.IsNullOrEmpty(ChecksumAlgorithm), 7, 1,
+ "The -Checksum and -ChecksumAlgorithm parameters require",
+ "The upload will proceed without checksum verification.");
TimeSpan timeout;
if (TimeoutSeconds.HasValue)
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/ImportPveOvaCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/ImportPveOvaCmdlet.cs
index 2c74acd..10898fa 100644
--- a/src/PSProxmoxVE/Cmdlets/Vms/ImportPveOvaCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Vms/ImportPveOvaCmdlet.cs
@@ -213,16 +213,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (!string.IsNullOrEmpty(uploadResult.Upid))
{
WriteVerbose("Waiting for OVA upload task to complete on PVE...");
- var completedUpload = taskService.WaitForTask(session, Node, uploadResult.Upid);
- if (completedUpload.ExitStatus != null && completedUpload.ExitStatus != "OK")
- {
- ThrowTerminatingError(new ErrorRecord(
- new InvalidOperationException($"OVA upload task failed with status: {completedUpload.ExitStatus}"),
- "OvaUploadFailed",
- ErrorCategory.InvalidResult,
- uploadResult.Upid));
- return;
- }
+ taskService.WaitForTask(session, Node, uploadResult.Upid);
}
// Step 5: Create VM with disks and network in a single API call.
@@ -298,16 +289,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent && !string.IsNullOrEmpty(createTask.Upid))
{
WriteVerbose("Waiting for VM creation + disk import to complete...");
- var completedCreate = taskService.WaitForTask(session, Node, createTask.Upid);
- if (completedCreate.ExitStatus != null && completedCreate.ExitStatus != "OK")
- {
- ThrowTerminatingError(new ErrorRecord(
- new InvalidOperationException($"OVA import task failed with status: {completedCreate.ExitStatus}"),
- "OvaImportFailed",
- ErrorCategory.InvalidResult,
- createTask.Upid));
- return;
- }
+ taskService.WaitForTask(session, Node, createTask.Upid);
}
// Step 8: Output the created VM
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
index 727f96d..915b0bc 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
@@ -50,6 +50,7 @@ namespace PSProxmoxVE.Core.Tests.Services
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
+ Assert.Equal("running", task.Status);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/vzdump",
It.Is>(d => d["vmid"] == "100" && d["storage"] == "local")),
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
index e5db43a..185220c 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
@@ -318,34 +318,6 @@ namespace PSProxmoxVE.Core.Tests.Services
Times.Once);
}
- [Fact]
- public void GetClusterStatus_ReturnsStatusArray()
- {
- // Arrange
- var json = @"{""data"": [
- {""type"": ""cluster"", ""name"": ""pve-cluster"", ""nodes"": 3, ""quorate"": 1, ""version"": 5},
- {""type"": ""node"", ""name"": ""pve1"", ""online"": 1, ""local"": 1, ""nodeid"": 1, ""ip"": ""10.0.0.1""}
- ]}";
- var mockClient = new Mock();
- mockClient.Setup(c => c.GetAsync("cluster/status")).ReturnsAsync(json);
- var service = new ClusterConfigService(mockClient.Object);
-
- // Act
- var statuses = service.GetClusterStatus(CreateSession());
-
- // Assert
- Assert.Equal(2, statuses.Length);
- Assert.Equal("cluster", statuses[0].Type);
- Assert.Equal("pve-cluster", statuses[0].Name);
- Assert.Equal(3, statuses[0].Nodes);
- Assert.Equal(1, statuses[0].Quorate);
- Assert.Equal("node", statuses[1].Type);
- Assert.Equal("pve1", statuses[1].Name);
- Assert.Equal(1, statuses[1].Online);
- Assert.Equal("10.0.0.1", statuses[1].Ip);
- mockClient.Verify(c => c.GetAsync("cluster/status"), Times.Once);
- }
-
[Fact]
public void GetNextId_ReturnsInt()
{
@@ -452,17 +424,6 @@ namespace PSProxmoxVE.Core.Tests.Services
Assert.Throws(() => service.GetNextId(null!));
}
- [Fact]
- public void GetClusterStatus_NullSession_ThrowsArgumentNullException()
- {
- // Arrange
- var mockClient = new Mock();
- var service = new ClusterConfigService(mockClient.Object);
-
- // Act & Assert
- Assert.Throws(() => service.GetClusterStatus(null!));
- }
-
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
index 4b01c07..49096ec 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
@@ -250,6 +250,7 @@ namespace PSProxmoxVE.Core.Tests.Services
Assert.NotNull(task);
Assert.Contains("startall", task.Upid);
Assert.Equal("pve1", task.Node);
+ Assert.Equal("running", task.Status);
mockClient.Verify(c => c.PostAsync(
"nodes/pve1/startall",
It.IsAny>()),
@@ -275,6 +276,7 @@ namespace PSProxmoxVE.Core.Tests.Services
Assert.NotNull(task);
Assert.Contains("stopall", task.Upid);
Assert.Equal("pve1", task.Node);
+ Assert.Equal("running", task.Status);
mockClient.Verify(c => c.PostAsync(
"nodes/pve1/stopall",
It.IsAny>()),
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/TemplateServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/TemplateServiceTests.cs
index 0e90373..2af0c22 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/TemplateServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/TemplateServiceTests.cs
@@ -40,6 +40,7 @@ namespace PSProxmoxVE.Core.Tests.Services
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
+ Assert.Equal("running", task.Status);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/qemu/{VmId}/template",
It.IsAny>()),
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
index bca3048..6be6fbe 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
@@ -130,6 +130,7 @@ namespace PSProxmoxVE.Core.Tests.Services
// config lock; the native endpoint keeps the whole restart server-side.
Assert.Equal($"nodes/{TestNode}/qemu/{TestVmId}/status/reboot", resource);
Assert.Contains("qmreboot", task.Upid);
+ Assert.Equal("running", task.Status);
}
[Fact]
diff --git a/tests/PSProxmoxVE.Core.Tests/TestHelper.cs b/tests/PSProxmoxVE.Core.Tests/TestHelper.cs
index fa16deb..45f68a5 100644
--- a/tests/PSProxmoxVE.Core.Tests/TestHelper.cs
+++ b/tests/PSProxmoxVE.Core.Tests/TestHelper.cs
@@ -1,10 +1,4 @@
using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Moq;
-using Moq.Protected;
namespace PSProxmoxVE.Core.Tests
{
@@ -15,38 +9,5 @@ namespace PSProxmoxVE.Core.Tests
var path = Path.Combine("Fixtures", filename);
return File.ReadAllText(path);
}
-
- public static Mock CreateMockHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK)
- {
- var mock = new Mock();
- mock.Protected()
- .Setup>("SendAsync",
- ItExpr.IsAny(),
- ItExpr.IsAny())
- .ReturnsAsync(new HttpResponseMessage
- {
- StatusCode = statusCode,
- Content = new StringContent(responseBody)
- });
- return mock;
- }
-
- public static Mock CreateMockHandlerSequence(params (string body, HttpStatusCode status)[] responses)
- {
- var mock = new Mock();
- var setup = mock.Protected()
- .SetupSequence>("SendAsync",
- ItExpr.IsAny(),
- ItExpr.IsAny());
- foreach (var (body, status) in responses)
- {
- setup = setup.ReturnsAsync(new HttpResponseMessage
- {
- StatusCode = status,
- Content = new StringContent(body)
- });
- }
- return mock;
- }
}
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Utilities/ApiValueHelperTests.cs b/tests/PSProxmoxVE.Core.Tests/Utilities/ApiValueHelperTests.cs
index 1d69253..88f955a 100644
--- a/tests/PSProxmoxVE.Core.Tests/Utilities/ApiValueHelperTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Utilities/ApiValueHelperTests.cs
@@ -9,7 +9,6 @@ namespace PSProxmoxVE.Core.Tests.Utilities
[Theory]
[InlineData(true)]
[InlineData(1L)]
- [InlineData(1)]
[InlineData("1")]
public void IsExited_TrueValues_ReturnsTrue(object value)
{
@@ -19,12 +18,12 @@ namespace PSProxmoxVE.Core.Tests.Utilities
[Theory]
[InlineData(false)]
[InlineData(0L)]
- [InlineData(0)]
[InlineData("0")]
[InlineData(null)]
[InlineData("")]
[InlineData("true")]
[InlineData(2L)]
+ [InlineData(1)]
[InlineData(42)]
public void IsExited_FalseValues_ReturnsFalse(object? value)
{
diff --git a/tests/PSProxmoxVE.Core.Tests/Utilities/PveTaskResponseTests.cs b/tests/PSProxmoxVE.Core.Tests/Utilities/PveTaskResponseTests.cs
new file mode 100644
index 0000000..8407af1
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Utilities/PveTaskResponseTests.cs
@@ -0,0 +1,45 @@
+using PSProxmoxVE.Core.Utilities;
+using Xunit;
+
+namespace PSProxmoxVE.Core.Tests.Utilities
+{
+ public class PveTaskResponseTests
+ {
+ [Fact]
+ public void Parse_UpidString_ReturnsRunningTaskWithNode()
+ {
+ var task = PveTaskResponse.Parse(
+ "{\"data\":\"UPID:pve1:00001234:00005678:AABBCCDD:qmstart:100:root@pam:\"}",
+ "pve1");
+
+ Assert.Equal("UPID:pve1:00001234:00005678:AABBCCDD:qmstart:100:root@pam:", task.Upid);
+ Assert.Equal("pve1", task.Node);
+ Assert.Equal("running", task.Status);
+ }
+
+ [Fact]
+ public void Parse_TaskObject_DeserializesAndStampsNode()
+ {
+ var task = PveTaskResponse.Parse(
+ "{\"data\":{\"upid\":\"UPID:pve2:00000001:00000002:00000003:vzdump::root@pam:\",\"status\":\"stopped\",\"exitstatus\":\"OK\",\"node\":\"ignored\"}}",
+ "pve2");
+
+ Assert.Equal("UPID:pve2:00000001:00000002:00000003:vzdump::root@pam:", task.Upid);
+ Assert.Equal("stopped", task.Status);
+ Assert.Equal("OK", task.ExitStatus);
+ Assert.Equal("pve2", task.Node);
+ }
+
+ [Theory]
+ [InlineData("{\"data\":null}")]
+ [InlineData("{}")]
+ public void Parse_NullOrAbsentData_ReturnsEmptyTaskWithNode(string json)
+ {
+ var task = PveTaskResponse.Parse(json, "pve3");
+
+ Assert.Equal(string.Empty, task.Upid);
+ Assert.Null(task.Status);
+ Assert.Equal("pve3", task.Node);
+ }
+ }
+}