mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-06 03:59:04 +00:00
* 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>
This commit is contained in:
committed by
GitHub
parent
109dca657a
commit
089bb7c81e
@@ -35,12 +35,6 @@ namespace PSProxmoxVE.Core.Client
|
||||
/// <summary>Synchronous wrapper for <see cref="PostAsync(string, Dictionary{string, string})"/>.</summary>
|
||||
string Post(string resource, Dictionary<string, string>? data = null);
|
||||
|
||||
/// <summary>Synchronous wrapper for <see cref="PutAsync"/>.</summary>
|
||||
string Put(string resource, Dictionary<string, string>? data = null);
|
||||
|
||||
/// <summary>Synchronous wrapper for <see cref="DeleteAsync"/>.</summary>
|
||||
string Delete(string resource);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to a Proxmox VE storage endpoint using MultipartFormDataContent.
|
||||
/// </summary>
|
||||
|
||||
@@ -20,9 +20,7 @@ namespace PSProxmoxVE.Core.Client
|
||||
/// </summary>
|
||||
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<string, string>? data = null) =>
|
||||
PostAsync(resource, data).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>Synchronous wrapper for <see cref="PutAsync"/>.</summary>
|
||||
public string Put(string resource, Dictionary<string, string>? data = null) =>
|
||||
PutAsync(resource, data).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>Synchronous wrapper for <see cref="DeleteAsync"/>.</summary>
|
||||
public string Delete(string resource) =>
|
||||
DeleteAsync(resource).GetAwaiter().GetResult();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ISO / file upload
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -434,9 +424,11 @@ namespace PSProxmoxVE.Core.Client
|
||||
private async Task<string> 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());
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace PSProxmoxVE.Core.Exceptions
|
||||
{
|
||||
/// <summary>Exception thrown when authentication to a Proxmox VE server fails.</summary>
|
||||
public class PveAuthenticationException : Exception
|
||||
{
|
||||
/// <summary>Initializes a new instance with the specified error message.</summary>
|
||||
/// <param name="message">The error message describing the authentication failure.</param>
|
||||
public PveAuthenticationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance with the specified error message and inner exception.</summary>
|
||||
/// <param name="message">The error message describing the authentication failure.</param>
|
||||
/// <param name="innerException">The exception that caused this failure.</param>
|
||||
public PveAuthenticationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class.
|
||||
/// </summary>
|
||||
public ClusterConfigService() { }
|
||||
public ClusterConfigService()
|
||||
{
|
||||
_clusterService = new ClusterService();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class with an injected HTTP client.
|
||||
/// </summary>
|
||||
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||
public ClusterConfigService(IPveHttpClient client) : base(client) { }
|
||||
public ClusterConfigService(IPveHttpClient client) : base(client)
|
||||
{
|
||||
_clusterService = new ClusterService(client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cluster configuration directory (GET /cluster/config).
|
||||
@@ -297,22 +305,6 @@ namespace PSProxmoxVE.Core.Services
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current cluster status (GET /cluster/status).
|
||||
/// Delegates to the same endpoint as <see cref="ClusterService.GetClusterStatus"/>.
|
||||
/// </summary>
|
||||
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<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocks until the cluster reports quorum (GET /cluster/status, quorate = 1).
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveTask>() ?? 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ namespace PSProxmoxVE.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check, typically from API response data.</param>
|
||||
/// <returns>True if the value represents an exited/true state, false otherwise.</returns>
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the task returned by a mutating Proxmox VE API call. Most such endpoints return
|
||||
/// the UPID as a bare string in <c>data</c>; a few return a task object directly.
|
||||
/// </summary>
|
||||
public static class PveTaskResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses <paramref name="json"/> as a Proxmox VE API envelope and extracts the task
|
||||
/// its <c>data</c> field describes, stamping <see cref="PveTask.Node"/> with
|
||||
/// <paramref name="node"/>.
|
||||
/// </summary>
|
||||
/// <param name="json">The raw JSON response body.</param>
|
||||
/// <param name="node">The cluster node the request was made against.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="PveTask"/> with <see cref="PveTask.Upid"/> and
|
||||
/// <see cref="PveTask.Status"/> set to <c>"running"</c> when <c>data</c> is a UPID
|
||||
/// string; the deserialized task when <c>data</c> is an object; or an empty task
|
||||
/// when <c>data</c> is null or absent.
|
||||
/// </returns>
|
||||
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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, object>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -92,6 +92,40 @@ namespace PSProxmoxVE.Cmdlets
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a soft warning when <paramref name="condition"/> holds and the connected
|
||||
/// server is below <paramref name="requiredMajor"/>.<paramref name="requiredMinor"/>.
|
||||
/// Unlike <see cref="RequireVersion"/>, this never blocks the call: the parameter or
|
||||
/// feature may simply be silently ignored by an older server.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="condition">True when the caller used the parameter/feature this warning covers.</param>
|
||||
/// <param name="requiredMajor">Major version the parameter/feature requires.</param>
|
||||
/// <param name="requiredMinor">Minor version the parameter/feature requires.</param>
|
||||
/// <param name="requirementClause">
|
||||
/// 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.".
|
||||
/// </param>
|
||||
/// <param name="consequence">Trailing sentence describing what happens if the server is too old.</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a PVE task to complete, then optionally polls VM status until
|
||||
/// it matches <paramref name="expectedStatus"/>. 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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Dictionary<string, string>>(d => d["vmid"] == "100" && d["storage"] == "local")),
|
||||
|
||||
@@ -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<IPveHttpClient>();
|
||||
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<ArgumentNullException>(() => service.GetNextId(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClusterStatus_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetClusterStatus(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||
{
|
||||
|
||||
@@ -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<Dictionary<string, string>>()),
|
||||
@@ -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<Dictionary<string, string>>()),
|
||||
|
||||
@@ -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<Dictionary<string, string>>()),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<HttpMessageHandler> CreateMockHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
var mock = new Mock<HttpMessageHandler>();
|
||||
mock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = statusCode,
|
||||
Content = new StringContent(responseBody)
|
||||
});
|
||||
return mock;
|
||||
}
|
||||
|
||||
public static Mock<HttpMessageHandler> CreateMockHandlerSequence(params (string body, HttpStatusCode status)[] responses)
|
||||
{
|
||||
var mock = new Mock<HttpMessageHandler>();
|
||||
var setup = mock.Protected()
|
||||
.SetupSequence<Task<HttpResponseMessage>>("SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
foreach (var (body, status) in responses)
|
||||
{
|
||||
setup = setup.ReturnsAsync(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = status,
|
||||
Content = new StringContent(body)
|
||||
});
|
||||
}
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user