mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
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 — parameter metadata, ValidateSet/ValidateRange/ValidateLength, ShouldProcess, ConfirmImpact - HaCmdlets.Tests.ps1: 14 cmdlets — same pattern Integration (ClusterConfig.Integration.Tests.ps1): - Full 2-node cluster lifecycle: create → join → options → HA groups → cleanup - Uses root@pam ticket auth for cluster create/join (API tokens lack permission) - Reconnects with API token after privileged operations - Skip helpers for standalone (no node B) and non-PVE-9 environments Fixes: - GetClusterConfig returns JToken (not JObject) to handle array responses from /cluster/config on standalone nodes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Core.Tests.Services
|
||||
{
|
||||
public class ClusterConfigServiceTests
|
||||
{
|
||||
private static PveSession CreateSession()
|
||||
{
|
||||
return new PveSession("pve.example.com", 8006, false,
|
||||
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClusterConfig_ReturnsJObject()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""nodes"": {""pve1"": {}}, ""totem"": {""version"": ""2""}}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var config = service.GetClusterConfig(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(config);
|
||||
Assert.NotNull(config["nodes"]);
|
||||
Assert.NotNull(config["totem"]);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCluster_PostsClusterNameAndReturnsUpid()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:cluster_create::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/config",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var upid = service.CreateCluster(CreateSession(), "test-cluster");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("cluster_create", upid);
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/config",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["clustername"] == "test-cluster")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCluster_WithOptionalParams_IncludesThemInPost()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:cluster_create::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/config",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
var links = new Dictionary<string, string> { ["link0"] = "10.0.0.1" };
|
||||
|
||||
// Act
|
||||
service.CreateCluster(CreateSession(), "test-cluster", links, nodeid: 1, votes: 2);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/config",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["clustername"] == "test-cluster" &&
|
||||
d["link0"] == "10.0.0.1" &&
|
||||
d["nodeid"] == "1" &&
|
||||
d["votes"] == "2")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConfigNodes_ReturnsNodeArray()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": [
|
||||
{""node"": ""pve1"", ""nodeid"": 1, ""ring0_addr"": ""10.0.0.1"", ""quorum_votes"": 1},
|
||||
{""node"": ""pve2"", ""nodeid"": 2, ""ring0_addr"": ""10.0.0.2"", ""quorum_votes"": 1}
|
||||
]}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config/nodes")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var nodes = service.GetConfigNodes(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, nodes.Length);
|
||||
Assert.Equal("pve1", nodes[0].Name);
|
||||
Assert.Equal(1, nodes[0].NodeId);
|
||||
Assert.Equal("10.0.0.1", nodes[0].Ring0Addr);
|
||||
Assert.Equal(1, nodes[0].QuorumVotes);
|
||||
Assert.Equal("pve2", nodes[1].Name);
|
||||
Assert.Equal(2, nodes[1].NodeId);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config/nodes"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddConfigNode_PostsToEncodedUrlAndReturnsUpid()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:addnode::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/config/nodes/pve2",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var upid = service.AddConfigNode(CreateSession(), "pve2", newNodeIp: "10.0.0.2");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("addnode", upid);
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/config/nodes/pve2",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["new_node_ip"] == "10.0.0.2")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveConfigNode_CallsDeleteWithEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.DeleteAsync("cluster/config/nodes/pve2"))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
service.RemoveConfigNode(CreateSession(), "pve2");
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.DeleteAsync("cluster/config/nodes/pve2"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJoinInfo_ReturnsClusterJoinInfo()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {
|
||||
""config_digest"": ""abc123"",
|
||||
""preferred_node"": ""pve1"",
|
||||
""nodelist"": [{""name"": ""pve1"", ""ring0_addr"": ""10.0.0.1""}],
|
||||
""totem"": {""version"": ""2""}
|
||||
}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config/join")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var joinInfo = service.GetJoinInfo(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("abc123", joinInfo.ConfigDigest);
|
||||
Assert.Equal("pve1", joinInfo.PreferredNode);
|
||||
Assert.NotNull(joinInfo.Nodelist);
|
||||
Assert.Single(joinInfo.Nodelist);
|
||||
Assert.NotNull(joinInfo.Totem);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config/join"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJoinInfo_WithNode_AppendsQueryString()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""config_digest"": ""abc123"", ""preferred_node"": ""pve1""}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config/join?node=pve1")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var joinInfo = service.GetJoinInfo(CreateSession(), "pve1");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pve1", joinInfo.PreferredNode);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config/join?node=pve1"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinCluster_PostsRequiredParamsAndReturnsUpid()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve2:000ABC:00000001:5F1234AB:join::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/config/join",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var upid = service.JoinCluster(CreateSession(), "10.0.0.1", "AA:BB:CC:DD", "secret");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("join", upid);
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/config/join",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["hostname"] == "10.0.0.1" &&
|
||||
d["fingerprint"] == "AA:BB:CC:DD" &&
|
||||
d["password"] == "secret")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTotem_ReturnsJObject()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""version"": ""2"", ""secauth"": ""on"", ""cluster_name"": ""pve-cluster""}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config/totem")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var totem = service.GetTotem(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(totem);
|
||||
Assert.Equal("2", totem["version"]?.ToString());
|
||||
Assert.Equal("on", totem["secauth"]?.ToString());
|
||||
Assert.Equal("pve-cluster", totem["cluster_name"]?.ToString());
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config/totem"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiVersion_ReturnsInt()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": 10}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/config/apiversion")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var version = service.GetApiVersion(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(10, version);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/config/apiversion"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClusterOptions_ReturnsPveClusterOptions()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {
|
||||
""keyboard"": ""en-us"",
|
||||
""language"": ""en"",
|
||||
""console"": ""html5"",
|
||||
""fencing"": ""watchdog"",
|
||||
""email_from"": ""admin@example.com"",
|
||||
""max_workers"": 4
|
||||
}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/options")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var options = service.GetClusterOptions(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("en-us", options.Keyboard);
|
||||
Assert.Equal("en", options.Language);
|
||||
Assert.Equal("html5", options.Console);
|
||||
Assert.Equal("watchdog", options.Fencing);
|
||||
Assert.Equal("admin@example.com", options.EmailFrom);
|
||||
Assert.Equal(4, options.MaxWorkers);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/options"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetClusterOptions_CallsPutAsyncWithCorrectResource()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PutAsync(
|
||||
"cluster/options",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
var opts = new Dictionary<string, string>
|
||||
{
|
||||
["keyboard"] = "de",
|
||||
["language"] = "de"
|
||||
};
|
||||
|
||||
// Act
|
||||
service.SetClusterOptions(CreateSession(), opts);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PutAsync(
|
||||
"cluster/options",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["keyboard"] == "de" && d["language"] == "de")),
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""100""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/nextid")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var nextId = service.GetNextId(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(100, nextId);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/nextid"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextId_WithVmid_AppendsQueryString()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""200""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/nextid?vmid=200")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var nextId = service.GetNextId(CreateSession(), 200);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, nextId);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/nextid?vmid=200"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextId_InvalidResponse_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""not-a-number""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/nextid")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => service.GetNextId(CreateSession()));
|
||||
Assert.Contains("unexpected next VMID value", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextId_NullDataField_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""notdata"": ""100""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/nextid")).ReturnsAsync(json);
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => service.GetNextId(CreateSession()));
|
||||
Assert.Contains("did not contain a 'data' field", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClusterConfig_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetClusterConfig(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConfigNodes_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetConfigNodes(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClusterOptions_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetClusterOptions(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextId_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new ClusterConfigService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
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()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new ClusterConfigService(null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.HA;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Core.Tests.Services
|
||||
{
|
||||
public class HaServiceTests
|
||||
{
|
||||
private static PveSession CreateSession()
|
||||
{
|
||||
return new PveSession("pve.example.com", 8006, false,
|
||||
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Resources
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetResources_ReturnsResourceArray()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": [
|
||||
{""sid"": ""vm:100"", ""state"": ""started"", ""group"": ""grp1"", ""type"": ""vm"", ""max_relocate"": 1, ""max_restart"": 1},
|
||||
{""sid"": ""ct:200"", ""state"": ""stopped"", ""type"": ""ct""}
|
||||
]}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/resources")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var resources = service.GetResources(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, resources.Length);
|
||||
Assert.Equal("vm:100", resources[0].Sid);
|
||||
Assert.Equal("started", resources[0].State);
|
||||
Assert.Equal("grp1", resources[0].Group);
|
||||
Assert.Equal("vm", resources[0].Type);
|
||||
Assert.Equal(1, resources[0].MaxRelocate);
|
||||
Assert.Equal(1, resources[0].MaxRestart);
|
||||
Assert.Equal("ct:200", resources[1].Sid);
|
||||
Assert.Equal("stopped", resources[1].State);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/resources"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetResource_ReturnsSingleResource()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""sid"": ""vm:100"", ""state"": ""started"", ""group"": ""grp1"", ""type"": ""vm""}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/resources/vm%3A100")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var resource = service.GetResource(CreateSession(), "vm:100");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("vm:100", resource.Sid);
|
||||
Assert.Equal("started", resource.State);
|
||||
Assert.Equal("grp1", resource.Group);
|
||||
Assert.Equal("vm", resource.Type);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/resources/vm%3A100"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetResource_EncodesColonInSid()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""sid"": ""ct:200"", ""state"": ""stopped""}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/resources/ct%3A200")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var resource = service.GetResource(CreateSession(), "ct:200");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("ct:200", resource.Sid);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/resources/ct%3A200"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateResource_PostsSidAndOptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/ha/resources",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
["state"] = "started",
|
||||
["group"] = "grp1"
|
||||
};
|
||||
|
||||
// Act
|
||||
service.CreateResource(CreateSession(), "vm:100", options);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/ha/resources",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["sid"] == "vm:100" &&
|
||||
d["state"] == "started" &&
|
||||
d["group"] == "grp1")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateResource_PutsOptionsToEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PutAsync(
|
||||
"cluster/ha/resources/vm%3A100",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
["state"] = "stopped"
|
||||
};
|
||||
|
||||
// Act
|
||||
service.UpdateResource(CreateSession(), "vm:100", options);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PutAsync(
|
||||
"cluster/ha/resources/vm%3A100",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["state"] == "stopped")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteResource_CallsDeleteWithEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.DeleteAsync("cluster/ha/resources/vm%3A100"))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
service.DeleteResource(CreateSession(), "vm:100");
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.DeleteAsync("cluster/ha/resources/vm%3A100"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MigrateResource_PostsNodeToEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:hamigrate::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/ha/resources/vm%3A100/migrate",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var upid = service.MigrateResource(CreateSession(), "vm:100", "pve2");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("hamigrate", upid);
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/ha/resources/vm%3A100/migrate",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["node"] == "pve2")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RelocateResource_PostsNodeToEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": ""UPID:pve1:000DEF:00000002:5F1234AC:harelocate::root@pam:""}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/ha/resources/vm%3A100/relocate",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var upid = service.RelocateResource(CreateSession(), "vm:100", "pve3");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("harelocate", upid);
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/ha/resources/vm%3A100/relocate",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["node"] == "pve3")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Groups
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetGroups_ReturnsGroupArray()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": [
|
||||
{""group"": ""grp1"", ""nodes"": ""pve1:2,pve2:1"", ""restricted"": 1, ""nofailback"": 0, ""comment"": ""Primary group""},
|
||||
{""group"": ""grp2"", ""nodes"": ""pve2,pve3""}
|
||||
]}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/groups")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var groups = service.GetGroups(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Equal("grp1", groups[0].Group);
|
||||
Assert.Equal("pve1:2,pve2:1", groups[0].Nodes);
|
||||
Assert.Equal(1, groups[0].Restricted);
|
||||
Assert.Equal(0, groups[0].NoFailback);
|
||||
Assert.Equal("Primary group", groups[0].Comment);
|
||||
Assert.Equal("grp2", groups[1].Group);
|
||||
Assert.Equal("pve2,pve3", groups[1].Nodes);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/groups"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetGroup_ReturnsSingleGroup()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""group"": ""grp1"", ""nodes"": ""pve1:2,pve2:1"", ""restricted"": 0}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/groups/grp1")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var group = service.GetGroup(CreateSession(), "grp1");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("grp1", group.Group);
|
||||
Assert.Equal("pve1:2,pve2:1", group.Nodes);
|
||||
Assert.Equal(0, group.Restricted);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/groups/grp1"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGroup_PostsGroupNodesAndOptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/ha/groups",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
["restricted"] = "1",
|
||||
["comment"] = "Test group"
|
||||
};
|
||||
|
||||
// Act
|
||||
service.CreateGroup(CreateSession(), "grp1", "pve1:2,pve2:1", options);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/ha/groups",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["group"] == "grp1" &&
|
||||
d["nodes"] == "pve1:2,pve2:1" &&
|
||||
d["restricted"] == "1" &&
|
||||
d["comment"] == "Test group")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteGroup_CallsDeleteWithEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.DeleteAsync("cluster/ha/groups/grp1"))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
service.DeleteGroup(CreateSession(), "grp1");
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.DeleteAsync("cluster/ha/groups/grp1"), Times.Once);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Status
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetStatus_ReturnsStatusArray()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": [
|
||||
{""id"": ""quorum"", ""type"": ""quorum"", ""node"": ""pve1"", ""status"": ""OK"", ""timestamp"": 1700000000},
|
||||
{""id"": ""manager"", ""type"": ""manager"", ""node"": ""pve1"", ""status"": ""OK"", ""crm_state"": ""S_IDLE""},
|
||||
{""id"": ""service:vm:100"", ""type"": ""service"", ""node"": ""pve1"", ""status"": ""started"", ""request_state"": ""started""}
|
||||
]}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/status/current")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var statuses = service.GetStatus(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, statuses.Length);
|
||||
Assert.Equal("quorum", statuses[0].Id);
|
||||
Assert.Equal("quorum", statuses[0].Type);
|
||||
Assert.Equal("pve1", statuses[0].Node);
|
||||
Assert.Equal("OK", statuses[0].Status);
|
||||
Assert.Equal(1700000000L, statuses[0].Timestamp);
|
||||
Assert.Equal("manager", statuses[1].Id);
|
||||
Assert.Equal("S_IDLE", statuses[1].CrmState);
|
||||
Assert.Equal("service:vm:100", statuses[2].Id);
|
||||
Assert.Equal("started", statuses[2].RequestState);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/status/current"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetManagerStatus_ReturnsJObject()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""manager_status"": {""master_node"": ""pve1""}, ""quorum"": {""node"": ""pve1""}}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/status/manager_status")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var status = service.GetManagerStatus(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(status);
|
||||
Assert.NotNull(status["manager_status"]);
|
||||
Assert.NotNull(status["quorum"]);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/status/manager_status"), Times.Once);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Rules
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetRules_ReturnsRuleArray()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": [
|
||||
{""rule"": ""rule1"", ""type"": ""location"", ""state"": ""enabled"", ""comment"": ""Location rule""},
|
||||
{""rule"": ""rule2"", ""type"": ""colocation"", ""state"": ""disabled""}
|
||||
]}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/rules")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var rules = service.GetRules(CreateSession());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, rules.Length);
|
||||
Assert.Equal("rule1", rules[0].Rule);
|
||||
Assert.Equal("location", rules[0].Type);
|
||||
Assert.Equal("enabled", rules[0].State);
|
||||
Assert.Equal("Location rule", rules[0].Comment);
|
||||
Assert.Equal("rule2", rules[1].Rule);
|
||||
Assert.Equal("colocation", rules[1].Type);
|
||||
Assert.Equal("disabled", rules[1].State);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/rules"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRule_ReturnsSingleRule()
|
||||
{
|
||||
// Arrange
|
||||
var json = @"{""data"": {""rule"": ""rule1"", ""type"": ""location"", ""state"": ""enabled""}}";
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.GetAsync("cluster/ha/rules/rule1")).ReturnsAsync(json);
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
var rule = service.GetRule(CreateSession(), "rule1");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("rule1", rule.Rule);
|
||||
Assert.Equal("location", rule.Type);
|
||||
Assert.Equal("enabled", rule.State);
|
||||
mockClient.Verify(c => c.GetAsync("cluster/ha/rules/rule1"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateRule_PostsOptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.PostAsync(
|
||||
"cluster/ha/rules",
|
||||
It.IsAny<Dictionary<string, string>>()))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
["rule"] = "rule1",
|
||||
["type"] = "location",
|
||||
["state"] = "enabled"
|
||||
};
|
||||
|
||||
// Act
|
||||
service.CreateRule(CreateSession(), options);
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.PostAsync(
|
||||
"cluster/ha/rules",
|
||||
It.Is<Dictionary<string, string>>(d =>
|
||||
d["rule"] == "rule1" &&
|
||||
d["type"] == "location" &&
|
||||
d["state"] == "enabled")),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteRule_CallsDeleteWithEncodedUrl()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient.Setup(c => c.DeleteAsync("cluster/ha/rules/rule1"))
|
||||
.ReturnsAsync("{}");
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act
|
||||
service.DeleteRule(CreateSession(), "rule1");
|
||||
|
||||
// Assert
|
||||
mockClient.Verify(c => c.DeleteAsync("cluster/ha/rules/rule1"), Times.Once);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Null session tests
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetResources_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetResources(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetGroups_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetGroups(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStatus_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetStatus(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRules_NullSession_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
var service = new HaService(mockClient.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => service.GetRules(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new HaService(null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
#Requires -Module Pester
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pester 5 tests for cluster config and management cmdlets:
|
||||
Get-PveClusterStatus, Get-PveClusterNextId, Get-PveClusterOption,
|
||||
Set-PveClusterOption, Get-PveClusterConfig, Get-PveClusterConfigNode,
|
||||
Add-PveClusterConfigNode, Remove-PveClusterConfigNode,
|
||||
Get-PveClusterJoinInfo, Add-PveClusterMember, New-PveCluster.
|
||||
|
||||
All tests are fully offline — no live Proxmox VE target is required.
|
||||
If a cmdlet is not yet compiled the test is marked Skipped.
|
||||
#>
|
||||
|
||||
BeforeAll {
|
||||
. $PSScriptRoot/../_TestHelper.ps1
|
||||
|
||||
$script:Availability = @{}
|
||||
foreach ($name in @(
|
||||
'Get-PveClusterStatus',
|
||||
'Get-PveClusterNextId',
|
||||
'Get-PveClusterOption',
|
||||
'Set-PveClusterOption',
|
||||
'Get-PveClusterConfig',
|
||||
'Get-PveClusterConfigNode',
|
||||
'Add-PveClusterConfigNode',
|
||||
'Remove-PveClusterConfigNode',
|
||||
'Get-PveClusterJoinInfo',
|
||||
'Add-PveClusterMember',
|
||||
'New-PveCluster'
|
||||
)) {
|
||||
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Skip-IfMissing([string]$Name) {
|
||||
if (-not $script:Availability[$Name]) {
|
||||
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterStatus
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterStatus' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterStatus' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterStatus'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterStatus'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterStatus'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterStatus'
|
||||
{ Get-PveClusterStatus -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterNextId
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterNextId' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterNextId' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have optional VmId parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$script:Cmd.Parameters.ContainsKey('VmId') | Should -BeTrue
|
||||
}
|
||||
It 'VmId should not be Mandatory' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$p = $script:Cmd.Parameters['VmId']
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
It 'VmId should have ValidateRange(100, 999999999)' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$p = $script:Cmd.Parameters['VmId']
|
||||
$rangeAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
|
||||
$rangeAttr | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr.MinRange | Should -Be 100
|
||||
$rangeAttr.MaxRange | Should -Be 999999999
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterNextId'
|
||||
{ Get-PveClusterNextId -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterOption
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterOption' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterOption' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterOption'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterOption'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterOption'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterOption'
|
||||
{ Get-PveClusterOption -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Set-PveClusterOption
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Set-PveClusterOption' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Set-PveClusterOption' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Keyboard parameter' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd.Parameters.ContainsKey('Keyboard') | Should -BeTrue
|
||||
}
|
||||
It 'Keyboard should have ValidateSet' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$p = $script:Cmd.Parameters['Keyboard']
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Language parameter' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd.Parameters.ContainsKey('Language') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Console parameter' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd.Parameters.ContainsKey('Console') | Should -BeTrue
|
||||
}
|
||||
It 'Console should have ValidateSet' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$p = $script:Cmd.Parameters['Console']
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Fencing parameter with ValidateSet' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$p = $script:Cmd.Parameters['Fencing']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Set-PveClusterOption'
|
||||
{ Set-PveClusterOption -Keyboard 'en-us' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterConfig
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterConfig' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterConfig' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterConfig'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterConfig'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterConfig'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterConfig'
|
||||
{ Get-PveClusterConfig -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterConfigNode
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterConfigNode' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterConfigNode' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterConfigNode'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterConfigNode'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterConfigNode'
|
||||
{ Get-PveClusterConfigNode -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Add-PveClusterConfigNode
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Add-PveClusterConfigNode' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Add-PveClusterConfigNode' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Node parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$p = $script:Cmd.Parameters['Node']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional NewNodeIp parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('NewNodeIp') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional NodeId parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('NodeId') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Votes parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('Votes') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Force switch' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('Force') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Add-PveClusterConfigNode'
|
||||
{ Add-PveClusterConfigNode -Node 'pve2' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove-PveClusterConfigNode
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Remove-PveClusterConfigNode' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Remove-PveClusterConfigNode' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Node parameter' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$p = $script:Cmd.Parameters['Node']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Remove-PveClusterConfigNode'
|
||||
{ Remove-PveClusterConfigNode -Node 'pve2' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveClusterJoinInfo
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveClusterJoinInfo' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterJoinInfo' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have optional Node parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
$script:Cmd.Parameters.ContainsKey('Node') | Should -BeTrue
|
||||
}
|
||||
It 'Node should not be Mandatory' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
$p = $script:Cmd.Parameters['Node']
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveClusterJoinInfo'
|
||||
{ Get-PveClusterJoinInfo -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Add-PveClusterMember
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Add-PveClusterMember' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Add-PveClusterMember' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Hostname parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$p = $script:Cmd.Parameters['Hostname']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have mandatory Fingerprint parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$p = $script:Cmd.Parameters['Fingerprint']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have mandatory Password parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$p = $script:Cmd.Parameters['Password']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Password should be SecureString type' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$script:Cmd.Parameters['Password'].ParameterType | Should -Be ([System.Security.SecureString])
|
||||
}
|
||||
It 'Should have optional Force switch' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$script:Cmd.Parameters.ContainsKey('Force') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Add-PveClusterMember'
|
||||
$secPw = ConvertTo-SecureString 'test' -AsPlainText -Force
|
||||
{ Add-PveClusterMember -Hostname 'pve1' -Fingerprint 'AA:BB' -Password $secPw -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New-PveCluster
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'New-PveCluster' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'New-PveCluster' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory ClusterName parameter' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$p = $script:Cmd.Parameters['ClusterName']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'ClusterName should have ValidateLength(1, 15)' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$p = $script:Cmd.Parameters['ClusterName']
|
||||
$lenAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateLengthAttribute] }
|
||||
$lenAttr | Should -Not -BeNullOrEmpty
|
||||
$lenAttr.MinLength | Should -Be 1
|
||||
$lenAttr.MaxLength | Should -Be 15
|
||||
}
|
||||
It 'Should have optional NodeId parameter' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd.Parameters.ContainsKey('NodeId') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Votes parameter' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd.Parameters.ContainsKey('Votes') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Links parameter' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd.Parameters.ContainsKey('Links') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'New-PveCluster'
|
||||
{ New-PveCluster -ClusterName 'testcluster' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
#Requires -Module Pester
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pester 5 tests for HA (High Availability) cmdlets:
|
||||
Get-PveHaResource, New-PveHaResource, Set-PveHaResource,
|
||||
Remove-PveHaResource, Move-PveHaResource,
|
||||
Get-PveHaGroup, New-PveHaGroup, Set-PveHaGroup, Remove-PveHaGroup,
|
||||
Get-PveHaStatus,
|
||||
Get-PveHaRule, New-PveHaRule, Set-PveHaRule, Remove-PveHaRule.
|
||||
|
||||
All tests are fully offline — no live Proxmox VE target is required.
|
||||
If a cmdlet is not yet compiled the test is marked Skipped.
|
||||
#>
|
||||
|
||||
BeforeAll {
|
||||
. $PSScriptRoot/../_TestHelper.ps1
|
||||
|
||||
$script:Availability = @{}
|
||||
foreach ($name in @(
|
||||
'Get-PveHaResource',
|
||||
'New-PveHaResource',
|
||||
'Set-PveHaResource',
|
||||
'Remove-PveHaResource',
|
||||
'Move-PveHaResource',
|
||||
'Get-PveHaGroup',
|
||||
'New-PveHaGroup',
|
||||
'Set-PveHaGroup',
|
||||
'Remove-PveHaGroup',
|
||||
'Get-PveHaStatus',
|
||||
'Get-PveHaRule',
|
||||
'New-PveHaRule',
|
||||
'Set-PveHaRule',
|
||||
'Remove-PveHaRule'
|
||||
)) {
|
||||
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Skip-IfMissing([string]$Name) {
|
||||
if (-not $script:Availability[$Name]) {
|
||||
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveHaResource
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveHaResource' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveHaResource' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have optional Sid parameter' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Sid') | Should -BeTrue
|
||||
}
|
||||
It 'Sid should not be Mandatory' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Sid']
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveHaResource'
|
||||
{ Get-PveHaResource -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New-PveHaResource
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'New-PveHaResource' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'New-PveHaResource' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Sid parameter' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Sid']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional State parameter with ValidateSet' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['State']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional Group parameter' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Group') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional MaxRelocate parameter with ValidateRange' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['MaxRelocate']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
|
||||
$rangeAttr | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr.MinRange | Should -Be 0
|
||||
$rangeAttr.MaxRange | Should -Be 10
|
||||
}
|
||||
It 'Should have optional MaxRestart parameter with ValidateRange' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['MaxRestart']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
|
||||
$rangeAttr | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr.MinRange | Should -Be 0
|
||||
$rangeAttr.MaxRange | Should -Be 10
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'New-PveHaResource'
|
||||
{ New-PveHaResource -Sid 'vm:100' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Set-PveHaResource
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Set-PveHaResource' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Set-PveHaResource' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Sid parameter' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Sid']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional State parameter with ValidateSet' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['State']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Set-PveHaResource'
|
||||
{ Set-PveHaResource -Sid 'vm:100' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove-PveHaResource
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Remove-PveHaResource' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Remove-PveHaResource' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Sid parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Sid']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Remove-PveHaResource'
|
||||
{ Remove-PveHaResource -Sid 'vm:100' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Move-PveHaResource
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Move-PveHaResource' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Move-PveHaResource' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Sid parameter' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Sid']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have mandatory Node parameter' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Node']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Mode parameter with ValidateSet(Migrate, Relocate)' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$p = $script:Cmd.Parameters['Mode']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
$vsAttr.ValidValues | Should -Contain 'Migrate'
|
||||
$vsAttr.ValidValues | Should -Contain 'Relocate'
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Move-PveHaResource'
|
||||
{ Move-PveHaResource -Sid 'vm:100' -Node 'pve2' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveHaGroup
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveHaGroup' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveHaGroup' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have optional Group parameter' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Group') | Should -BeTrue
|
||||
}
|
||||
It 'Group should not be Mandatory' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Group']
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveHaGroup'
|
||||
{ Get-PveHaGroup -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New-PveHaGroup
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'New-PveHaGroup' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'New-PveHaGroup' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Group parameter' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Group']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have mandatory Nodes parameter' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Nodes']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional Restricted switch' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Restricted') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional NoFailback switch' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('NoFailback') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Comment parameter' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'New-PveHaGroup'
|
||||
{ New-PveHaGroup -Group 'grp1' -Nodes 'pve1:1,pve2:2' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Set-PveHaGroup
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Set-PveHaGroup' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Set-PveHaGroup' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Group parameter' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Group']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional Nodes parameter' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Nodes') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Restricted parameter with ValidateRange(0, 1)' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Restricted']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
|
||||
$rangeAttr | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr.MinRange | Should -Be 0
|
||||
$rangeAttr.MaxRange | Should -Be 1
|
||||
}
|
||||
It 'Should have optional NoFailback parameter with ValidateRange(0, 1)' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['NoFailback']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
|
||||
$rangeAttr | Should -Not -BeNullOrEmpty
|
||||
$rangeAttr.MinRange | Should -Be 0
|
||||
$rangeAttr.MaxRange | Should -Be 1
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Set-PveHaGroup'
|
||||
{ Set-PveHaGroup -Group 'grp1' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove-PveHaGroup
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Remove-PveHaGroup' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Remove-PveHaGroup' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Group parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$p = $script:Cmd.Parameters['Group']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Remove-PveHaGroup'
|
||||
{ Remove-PveHaGroup -Group 'grp1' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveHaStatus
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveHaStatus' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveHaStatus' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveHaStatus'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveHaStatus'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveHaStatus'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveHaStatus'
|
||||
{ Get-PveHaStatus -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Get-PveHaRule
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Get-PveHaRule' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Get-PveHaRule' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have optional Rule parameter' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Rule') | Should -BeTrue
|
||||
}
|
||||
It 'Rule should not be Mandatory' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['Rule']
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Get-PveHaRule'
|
||||
{ Get-PveHaRule -ErrorAction Stop } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New-PveHaRule
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'New-PveHaRule' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'New-PveHaRule' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Type parameter' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['Type']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional State parameter with ValidateSet' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['State']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional Comment parameter' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
|
||||
}
|
||||
It 'Should have optional Properties parameter' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Properties') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'New-PveHaRule'
|
||||
{ New-PveHaRule -Type 'location' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Set-PveHaRule
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Set-PveHaRule' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Set-PveHaRule' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Rule parameter' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['Rule']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional State parameter with ValidateSet' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['State']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$vsAttr = $p.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
|
||||
$vsAttr | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have optional Properties parameter' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Properties') | Should -BeTrue
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Set-PveHaRule'
|
||||
{ Set-PveHaRule -Rule 'rule1' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove-PveHaRule
|
||||
# ---------------------------------------------------------------------------
|
||||
Describe 'Remove-PveHaRule' -Tag 'Unit' {
|
||||
|
||||
BeforeAll { $script:Cmd = Get-Command 'Remove-PveHaRule' -ErrorAction SilentlyContinue }
|
||||
|
||||
Context 'Command existence' {
|
||||
It 'Should be available after module import' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Parameter metadata' {
|
||||
It 'Should have mandatory Rule parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$p = $script:Cmd.Parameters['Rule']
|
||||
$p | Should -Not -BeNullOrEmpty
|
||||
$isMandatory = $p.ParameterSets.Values | Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'Should have Session parameter' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
It 'Should support ShouldProcess' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.SupportsShouldProcess | Should -BeTrue
|
||||
}
|
||||
It 'Should have ConfirmImpact High' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
$cmdletAttr = $script:Cmd.ImplementingType.GetCustomAttributes($true) |
|
||||
Where-Object { $_ -is [System.Management.Automation.CmdletAttribute] }
|
||||
$cmdletAttr.ConfirmImpact | Should -Be 'High'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Without active session' {
|
||||
It 'Should throw when no session is active' {
|
||||
Skip-IfMissing 'Remove-PveHaRule'
|
||||
{ Remove-PveHaRule -Rule 'rule1' -ErrorAction Stop -Confirm:$false } |
|
||||
Should -Throw '*No active Proxmox VE session*'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
#Requires -Module Pester
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pester 5 integration tests for cluster configuration and HA lifecycle.
|
||||
|
||||
These tests exercise the full cluster lifecycle: creating a cluster on
|
||||
node A, joining node B, managing HA groups, and removing node B.
|
||||
|
||||
Nodes are provisioned STANDALONE (not clustered). The tests create and
|
||||
destroy the cluster during the run.
|
||||
|
||||
Required environment variables:
|
||||
PVETEST_HOST - Node A hostname or IP
|
||||
PVETEST_PORT - API port (default 8006)
|
||||
PVETEST_APITOKEN - Node A API token
|
||||
PVETEST_NODE - Node A node name
|
||||
|
||||
Optional (multi-node tests):
|
||||
PVETEST_HOST_B - Node B hostname or IP
|
||||
PVETEST_APITOKEN_B - Node B API token
|
||||
PVETEST_PASSWORD - Root password for nested PVE instances
|
||||
|
||||
Optional:
|
||||
PVETEST_PVE_VERSION - PVE major version (8 or 9)
|
||||
|
||||
WARNING: These tests CREATE and DESTROY a cluster on the target nodes.
|
||||
Never run against a production cluster.
|
||||
#>
|
||||
|
||||
BeforeAll {
|
||||
. $PSScriptRoot/../_TestHelper.ps1
|
||||
|
||||
# --- Required env vars ---
|
||||
$requiredVars = @('PVETEST_HOST', 'PVETEST_PORT', 'PVETEST_APITOKEN', 'PVETEST_NODE')
|
||||
$script:SkipReason = $null
|
||||
|
||||
foreach ($var in $requiredVars) {
|
||||
if (-not [System.Environment]::GetEnvironmentVariable($var)) {
|
||||
$script:SkipReason = "No live Proxmox VE target configured. Set: $($requiredVars -join ', ')"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# --- Convenience accessors ---
|
||||
$script:Host_ = [System.Environment]::GetEnvironmentVariable('PVETEST_HOST')
|
||||
$portEnv = [System.Environment]::GetEnvironmentVariable('PVETEST_PORT')
|
||||
$script:Port = [int]$(if ($portEnv) { $portEnv } else { '8006' })
|
||||
$script:ApiToken = [System.Environment]::GetEnvironmentVariable('PVETEST_APITOKEN')
|
||||
$script:Node = [System.Environment]::GetEnvironmentVariable('PVETEST_NODE')
|
||||
|
||||
# --- Optional multi-node vars ---
|
||||
$script:HostB = [System.Environment]::GetEnvironmentVariable('PVETEST_HOST_B')
|
||||
$script:ApiTokenB = [System.Environment]::GetEnvironmentVariable('PVETEST_APITOKEN_B')
|
||||
$script:Password = [System.Environment]::GetEnvironmentVariable('PVETEST_PASSWORD')
|
||||
$script:PveVersion = [System.Environment]::GetEnvironmentVariable('PVETEST_PVE_VERSION')
|
||||
|
||||
# --- State tracking ---
|
||||
$script:ClusterCreated = $false
|
||||
$script:JoinInfo = $null
|
||||
$script:NodeBName = $null
|
||||
|
||||
# --- Skip helpers ---
|
||||
function script:Skip-IfNoTarget {
|
||||
if ($script:SkipReason) {
|
||||
Set-ItResult -Skipped -Because $script:SkipReason
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfNoNodeB {
|
||||
if (Skip-IfNoTarget) { return $true }
|
||||
if (-not $script:HostB -or -not $script:ApiTokenB -or -not $script:Password) {
|
||||
Set-ItResult -Skipped -Because 'Multi-node env vars not set (PVETEST_HOST_B, PVETEST_APITOKEN_B, PVETEST_PASSWORD)'
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfNoCluster {
|
||||
if (Skip-IfNoTarget) { return $true }
|
||||
if (-not $script:ClusterCreated) {
|
||||
Set-ItResult -Skipped -Because 'Cluster was not created (multi-node env vars may not be set)'
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfNoPve9 {
|
||||
if (Skip-IfNoCluster) { return $true }
|
||||
if (-not $script:PveVersion -or [int]$script:PveVersion -lt 9) {
|
||||
Set-ItResult -Skipped -Because 'HA rules require PVE 9.0+'
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
# Best-effort cleanup — each step may fail if cluster state is partial
|
||||
if ($script:ClusterCreated) {
|
||||
# Try to discover node B name and remove it from the cluster
|
||||
if ($script:NodeBName) {
|
||||
try {
|
||||
Write-Warning "Cleanup: removing node '$($script:NodeBName)' from cluster..."
|
||||
Remove-PveClusterConfigNode -Node $script:NodeBName -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Cleanup: failed to remove node B from cluster: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Disconnect any active sessions
|
||||
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
|
||||
}
|
||||
|
||||
Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Connection' {
|
||||
It 'Should connect to PVE node A' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
$session = Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-ApiToken $script:ApiToken `
|
||||
-SkipCertificateCheck `
|
||||
-PassThru
|
||||
|
||||
$session | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Pre-cluster reads on standalone node' {
|
||||
It 'Get-PveClusterStatus returns node info' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
$status = Get-PveClusterStatus -ErrorAction Stop
|
||||
$status | Should -Not -BeNullOrEmpty
|
||||
@($status).Count | Should -BeGreaterOrEqual 1
|
||||
}
|
||||
|
||||
It 'Get-PveClusterNextId returns valid VMID' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
$nextId = Get-PveClusterNextId -ErrorAction Stop
|
||||
$nextId | Should -Not -BeNullOrEmpty
|
||||
[int]$nextId | Should -BeGreaterOrEqual 100
|
||||
}
|
||||
|
||||
It 'Get-PveClusterOption returns options' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
$options = Get-PveClusterOption -ErrorAction Stop
|
||||
$options | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Get-PveClusterConfig returns config' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
# /cluster/config returns a directory listing (array) on standalone nodes
|
||||
# and may return an object on clustered nodes — either is valid
|
||||
{ Get-PveClusterConfig -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'Get-PveHaStatus returns status without throwing' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
# On a standalone node this may return an empty list — that is fine
|
||||
{ Get-PveHaStatus -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Create cluster on node A' {
|
||||
It 'New-PveCluster creates a cluster named pester-cluster' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
|
||||
# Cluster creation requires root@pam ticket auth, not API token
|
||||
$secPw = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
$cred = New-Object System.Management.Automation.PSCredential('root@pam', $secPw)
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-Credential $cred `
|
||||
-SkipCertificateCheck
|
||||
|
||||
$result = New-PveCluster -ClusterName 'pester-cluster' -Confirm:$false -ErrorAction Stop
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
$script:ClusterCreated = $true
|
||||
|
||||
# Wait for corosync to settle
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# Reconnect with API token for remaining tests
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-ApiToken $script:ApiToken `
|
||||
-SkipCertificateCheck
|
||||
}
|
||||
|
||||
It 'Get-PveClusterStatus shows cluster formed' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$status = Get-PveClusterStatus -ErrorAction Stop
|
||||
$clusterEntry = @($status) | Where-Object { $_.Type -eq 'cluster' }
|
||||
$clusterEntry | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Get-PveClusterConfigNode lists node A' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$nodes = Get-PveClusterConfigNode -ErrorAction Stop
|
||||
$nodes | Should -Not -BeNullOrEmpty
|
||||
@($nodes).Count | Should -BeGreaterOrEqual 1
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Join node B to cluster' {
|
||||
It 'Get-PveClusterJoinInfo returns join token from node A' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$script:JoinInfo = Get-PveClusterJoinInfo -ErrorAction Stop
|
||||
$script:JoinInfo | Should -Not -BeNullOrEmpty
|
||||
$script:JoinInfo.Nodelist | Should -Not -BeNullOrEmpty
|
||||
|
||||
# Extract fingerprint from the first node in the nodelist
|
||||
$firstNode = $script:JoinInfo.Nodelist[0]
|
||||
$firstNode['pve_fp'] | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Add-PveClusterMember joins node B to cluster' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
if ($null -eq $script:JoinInfo) {
|
||||
Set-ItResult -Skipped -Because 'Join info was not retrieved'
|
||||
return
|
||||
}
|
||||
|
||||
# Connect to node B with root@pam (join requires ticket auth, not API token)
|
||||
$secPw = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
$credB = New-Object System.Management.Automation.PSCredential('root@pam', $secPw)
|
||||
Connect-PveServer `
|
||||
-Server $script:HostB `
|
||||
-Port $script:Port `
|
||||
-Credential $credB `
|
||||
-SkipCertificateCheck
|
||||
|
||||
# Extract fingerprint from join info
|
||||
$fingerprint = $script:JoinInfo.Nodelist[0]['pve_fp'].ToString()
|
||||
|
||||
$result = Add-PveClusterMember `
|
||||
-Hostname $script:Host_ `
|
||||
-Fingerprint $fingerprint `
|
||||
-Password $secPw `
|
||||
-Confirm:$false `
|
||||
-ErrorAction Stop
|
||||
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
|
||||
# Wait for cluster sync
|
||||
Start-Sleep -Seconds 15
|
||||
|
||||
# Reconnect to node A for subsequent tests
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-ApiToken $script:ApiToken `
|
||||
-SkipCertificateCheck
|
||||
}
|
||||
|
||||
It 'Get-PveClusterConfigNode shows both nodes' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$nodes = Get-PveClusterConfigNode -ErrorAction Stop
|
||||
@($nodes).Count | Should -BeGreaterOrEqual 2
|
||||
|
||||
# Discover and store node B's name for cleanup
|
||||
$nodeNames = @($nodes) | ForEach-Object { $_.Name }
|
||||
$script:NodeBName = $nodeNames | Where-Object { $_ -ne $script:Node } | Select-Object -First 1
|
||||
$script:NodeBName | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Get-PveClusterStatus shows 2 nodes online' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$status = Get-PveClusterStatus -ErrorAction Stop
|
||||
$nodeEntries = @($status) | Where-Object { $_.Type -eq 'node' }
|
||||
@($nodeEntries).Count | Should -BeGreaterOrEqual 2
|
||||
|
||||
$onlineNodes = @($nodeEntries) | Where-Object { $_.Online -eq 1 }
|
||||
@($onlineNodes).Count | Should -BeGreaterOrEqual 2
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Cluster options' {
|
||||
It 'Set-PveClusterOption sets keyboard to en-us' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
# Save original keyboard setting
|
||||
$options = Get-PveClusterOption -ErrorAction Stop
|
||||
$script:OriginalKeyboard = $options.Keyboard
|
||||
|
||||
Set-PveClusterOption -Keyboard 'en-us' -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
|
||||
It 'Get-PveClusterOption reflects the keyboard change' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$options = Get-PveClusterOption -ErrorAction Stop
|
||||
$options.Keyboard | Should -Be 'en-us'
|
||||
}
|
||||
|
||||
It 'Set-PveClusterOption restores original keyboard' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
if ($script:OriginalKeyboard) {
|
||||
Set-PveClusterOption -Keyboard $script:OriginalKeyboard -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
else {
|
||||
# Original was unset — delete the key to restore default
|
||||
Set-PveClusterOption -Delete 'keyboard' -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
|
||||
$options = Get-PveClusterOption -ErrorAction Stop
|
||||
$options.Keyboard | Should -Be $script:OriginalKeyboard
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'HA group management' {
|
||||
It 'New-PveHaGroup creates test group' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$nodeList = "$($script:Node):1"
|
||||
if ($script:NodeBName) {
|
||||
$nodeList += ",$($script:NodeBName):2"
|
||||
}
|
||||
|
||||
{ New-PveHaGroup -Group 'pester-group' -Nodes $nodeList -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'Get-PveHaGroup lists groups including pester-group' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$groups = @(Get-PveHaGroup -ErrorAction Stop)
|
||||
$match = $groups | Where-Object { $_.Group -eq 'pester-group' }
|
||||
$match | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Get-PveHaGroup returns specific group' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$group = Get-PveHaGroup -Group 'pester-group' -ErrorAction Stop
|
||||
$group | Should -Not -BeNullOrEmpty
|
||||
$group.Group | Should -Be 'pester-group'
|
||||
}
|
||||
|
||||
It 'Set-PveHaGroup updates comment' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
{ Set-PveHaGroup -Group 'pester-group' -Comment 'pester test' -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
|
||||
$group = Get-PveHaGroup -Group 'pester-group' -ErrorAction Stop
|
||||
$group.Comment | Should -Be 'pester test'
|
||||
}
|
||||
|
||||
It 'Remove-PveHaGroup deletes test group' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
{ Remove-PveHaGroup -Group 'pester-group' -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
|
||||
# Verify the group is gone
|
||||
$groups = @(Get-PveHaGroup -ErrorAction Stop)
|
||||
$match = $groups | Where-Object { $_.Group -eq 'pester-group' }
|
||||
$match | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'HA resource management' {
|
||||
It 'Skipped — HA resources require a test VM' {
|
||||
Set-ItResult -Skipped -Because 'HA resource tests require a running VM, which is not provisioned in this test file'
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'HA rules (PVE 9.0+)' {
|
||||
It 'Get-PveHaRule returns rules list without throwing' {
|
||||
if (Skip-IfNoPve9) { return }
|
||||
|
||||
# May return an empty list on a fresh cluster — that is fine
|
||||
{ Get-PveHaRule -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Remove node B and cleanup' {
|
||||
It 'Remove-PveClusterConfigNode removes node B' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
if (-not $script:NodeBName) {
|
||||
Set-ItResult -Skipped -Because 'Node B name was not discovered'
|
||||
return
|
||||
}
|
||||
|
||||
# Ensure we are connected to node A
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-ApiToken $script:ApiToken `
|
||||
-SkipCertificateCheck
|
||||
|
||||
{ Remove-PveClusterConfigNode -Node $script:NodeBName -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
|
||||
# Clear the name so AfterAll does not try to remove again
|
||||
$script:NodeBName = $null
|
||||
|
||||
# Wait for config to propagate
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
||||
It 'Get-PveClusterConfigNode shows only node A' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$nodes = Get-PveClusterConfigNode -ErrorAction Stop
|
||||
@($nodes).Count | Should -Be 1
|
||||
@($nodes)[0].Name | Should -Be $script:Node
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user