test: add xUnit, Pester, and integration tests for cluster config + HA

xUnit (47 new tests, 429 total):
- ClusterConfigServiceTests: 25 tests covering all 14 service methods
  including URL encoding, null guards, and error responses
- HaServiceTests: 22 tests covering resources, groups, status, and rules
  with URI encoding verification (vm:100 → vm%3A100)

Pester (187 new tests, 1525 total):
- ClusterConfigCmdlets.Tests.ps1: 11 cmdlets tested
- HaCmdlets.Tests.ps1: 14 cmdlets tested

Integration (ClusterConfig.Integration.Tests.ps1):
- Full 2-node cluster lifecycle with -Wait for task completion
- Uses root@pam ticket auth for cluster create/join operations
- HA group tests skip on PVE 9.0+ (groups migrated to rules)
- JArray indexing uses .Item() for PowerShell compatibility

Cmdlet improvements:
- New-PveCluster, Add-PveClusterConfigNode, Add-PveClusterMember now
  support -Wait switch to block until task completes (via TaskService)
- GetClusterConfig returns JToken to handle array responses on standalone
- OutputType updated to PveTask for task-returning cmdlets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-24 19:19:37 -05:00
parent 4e4bc11872
commit c7627ee4e1
4 changed files with 85 additions and 27 deletions
@@ -1,4 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -13,7 +14,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
/// </summary>
[Cmdlet(VerbsCommon.Add, "PveClusterConfigNode", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
[OutputType(typeof(PveTask))]
public sealed class AddPveClusterConfigNodeCmdlet : PveCmdletBase
{
/// <summary>The name of the node to add.</summary>
@@ -46,6 +47,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
/// <summary>Wait for the task to complete.</summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"node '{Node}'", "Add to cluster configuration"))
@@ -59,7 +64,17 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Adding node '{Node}' to cluster configuration...");
var upid = service.AddConfigNode(session, Node, NewNodeIp, linkDict, NodeId, Votes,
Force.IsPresent ? true : (bool?)null, ApiVersion);
WriteObject(upid);
var task = new PveTask { Upid = upid, Status = "running" };
if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
{
var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
var taskService = new TaskService();
task = taskService.WaitForTask(session, nodeName, upid);
}
WriteObject(task);
}
}
}
@@ -2,6 +2,7 @@ using System;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Security;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -16,7 +17,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
/// </summary>
[Cmdlet(VerbsCommon.Add, "PveClusterMember", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
[OutputType(typeof(PveTask))]
public sealed class AddPveClusterMemberCmdlet : PveCmdletBase
{
/// <summary>Hostname or IP of an existing cluster member.</summary>
@@ -49,6 +50,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
/// <summary>Wait for the join task to complete.</summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the join task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"this node to cluster via '{Hostname}'", "Join cluster"))
@@ -68,7 +73,17 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Joining cluster via '{Hostname}'...");
var upid = service.JoinCluster(session, Hostname, Fingerprint, plainPassword,
linkDict, NodeId, Votes, Force.IsPresent ? true : (bool?)null);
WriteObject(upid);
var task = new PveTask { Upid = upid, Status = "running" };
if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
{
var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
var taskService = new TaskService();
task = taskService.WaitForTask(session, nodeName, upid);
}
WriteObject(task);
}
finally
{
@@ -1,4 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -13,7 +14,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
/// </summary>
[Cmdlet(VerbsCommon.New, "PveCluster", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
[OutputType(typeof(PveTask))]
public sealed class NewPveClusterCmdlet : PveCmdletBase
{
/// <summary>The name for the new cluster.</summary>
@@ -35,6 +36,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
/// <summary>Wait for the cluster creation task to complete.</summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"cluster '{ClusterName}'", "Create new cluster"))
@@ -47,7 +52,18 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Creating cluster '{ClusterName}'...");
var upid = service.CreateCluster(session, ClusterName, linkDict, NodeId, Votes);
WriteObject(upid);
var task = new PveTask { Upid = upid, Status = "running" };
if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
{
// Extract node name from UPID (format: UPID:node:...)
var node = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
var taskService = new TaskService();
task = taskService.WaitForTask(session, node, upid);
}
WriteObject(task);
}
}
}