diff --git a/src/PSProxmoxVE/Cmdlets/Firewall/GetPveFirewallRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Firewall/GetPveFirewallRuleCmdlet.cs index 6babe06..4a5c35e 100644 --- a/src/PSProxmoxVE/Cmdlets/Firewall/GetPveFirewallRuleCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Firewall/GetPveFirewallRuleCmdlet.cs @@ -10,8 +10,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall [OutputType(typeof(PveFirewallRule))] public sealed class GetPveFirewallRuleCmdlet : PveCmdletBase { - [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")] - [ValidateSet("Cluster", "Node", "Vm", "Container")] + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, Container, or Group.")] + [ValidateSet("Cluster", "Node", "Vm", "Container", "Group")] public string Level { get; set; } = string.Empty; [Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")] @@ -21,13 +21,17 @@ namespace PSProxmoxVE.Cmdlets.Firewall [ValidateRange(100, 999999999)] public int? VmId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "The security group name. Required when Level is Group.")] + public string? Group { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Optional rule position to filter by.")] public int? Position { get; set; } protected override void ProcessRecord() { var level = Level; - if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase) && + !string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(Node)) { @@ -48,13 +52,25 @@ namespace PSProxmoxVE.Cmdlets.Firewall return; } } + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(Group)) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Group is required when Level is Group."), + "GroupRequired", ErrorCategory.InvalidArgument, null)); + return; + } + } var session = GetSession(); var service = new FirewallService(); var vmid = VmId; WriteVerbose($"Getting firewall rules at level '{level}'..."); - var rules = service.GetRules(session, level, Node, vmid); + var rules = string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase) + ? service.GetGroupRules(session, Group!) + : service.GetRules(session, level, Node, vmid); if (Position.HasValue) { diff --git a/src/PSProxmoxVE/Cmdlets/Firewall/NewPveFirewallRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Firewall/NewPveFirewallRuleCmdlet.cs index 203881f..c349196 100644 --- a/src/PSProxmoxVE/Cmdlets/Firewall/NewPveFirewallRuleCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Firewall/NewPveFirewallRuleCmdlet.cs @@ -10,8 +10,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall [OutputType(typeof(PveFirewallRule))] public sealed class NewPveFirewallRuleCmdlet : PveCmdletBase { - [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")] - [ValidateSet("Cluster", "Node", "Vm", "Container")] + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, Container, or Group.")] + [ValidateSet("Cluster", "Node", "Vm", "Container", "Group")] public string Level { get; set; } = string.Empty; [Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")] @@ -21,6 +21,9 @@ namespace PSProxmoxVE.Cmdlets.Firewall [ValidateRange(100, 999999999)] public int? VmId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "The security group name. Required when Level is Group.")] + public string? Group { get; set; } + [Parameter(Mandatory = true, HelpMessage = "The rule type: in, out, or group.")] [ValidateSet("in", "out", "group")] public string Type { get; set; } = string.Empty; @@ -62,7 +65,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall protected override void ProcessRecord() { var level = Level; - if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase) && + !string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(Node)) { @@ -83,8 +87,21 @@ namespace PSProxmoxVE.Cmdlets.Firewall return; } } + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(Group)) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Group is required when Level is Group."), + "GroupRequired", ErrorCategory.InvalidArgument, null)); + return; + } + } - if (!ShouldProcess($"firewall rule ({Level})", "Create")) + var target = string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase) + ? $"firewall rule ({Level} '{Group}')" + : $"firewall rule ({Level})"; + if (!ShouldProcess(target, "Create")) return; var session = GetSession(); @@ -119,7 +136,10 @@ namespace PSProxmoxVE.Cmdlets.Firewall config["iface"] = Iface!; WriteVerbose($"Creating firewall rule at level '{level}'..."); - service.CreateRule(session, level, config, Node, vmid); + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + service.CreateGroupRule(session, Group!, config); + else + service.CreateRule(session, level, config, Node, vmid); } } } diff --git a/src/PSProxmoxVE/Cmdlets/Firewall/RemovePveFirewallRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Firewall/RemovePveFirewallRuleCmdlet.cs index 6114f11..4d4fb99 100644 --- a/src/PSProxmoxVE/Cmdlets/Firewall/RemovePveFirewallRuleCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Firewall/RemovePveFirewallRuleCmdlet.cs @@ -9,8 +9,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall [OutputType(typeof(void))] public sealed class RemovePveFirewallRuleCmdlet : PveCmdletBase { - [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")] - [ValidateSet("Cluster", "Node", "Vm", "Container")] + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, Container, or Group.")] + [ValidateSet("Cluster", "Node", "Vm", "Container", "Group")] public string Level { get; set; } = string.Empty; [Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")] @@ -20,13 +20,17 @@ namespace PSProxmoxVE.Cmdlets.Firewall [ValidateRange(100, 999999999)] public int? VmId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "The security group name. Required when Level is Group.")] + public string? Group { get; set; } + [Parameter(Mandatory = true, HelpMessage = "The rule position to remove.")] public int Position { get; set; } protected override void ProcessRecord() { var level = Level; - if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase) && + !string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(Node)) { @@ -47,8 +51,21 @@ namespace PSProxmoxVE.Cmdlets.Firewall return; } } + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(Group)) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Group is required when Level is Group."), + "GroupRequired", ErrorCategory.InvalidArgument, null)); + return; + } + } - if (!ShouldProcess($"firewall rule at position {Position} ({Level})", "Remove")) + var target = string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase) + ? $"firewall rule at position {Position} ({Level} '{Group}')" + : $"firewall rule at position {Position} ({Level})"; + if (!ShouldProcess(target, "Remove")) return; var session = GetSession(); @@ -56,7 +73,10 @@ namespace PSProxmoxVE.Cmdlets.Firewall var vmid = VmId; WriteVerbose($"Removing firewall rule at position {Position} ({level})..."); - service.RemoveRule(session, level, Position, Node, vmid); + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + service.RemoveGroupRule(session, Group!, Position); + else + service.RemoveRule(session, level, Position, Node, vmid); } } } diff --git a/src/PSProxmoxVE/Cmdlets/Firewall/SetPveFirewallRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Firewall/SetPveFirewallRuleCmdlet.cs index 9383e48..5eea509 100644 --- a/src/PSProxmoxVE/Cmdlets/Firewall/SetPveFirewallRuleCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Firewall/SetPveFirewallRuleCmdlet.cs @@ -10,8 +10,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall [OutputType(typeof(void))] public sealed class SetPveFirewallRuleCmdlet : PveCmdletBase { - [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")] - [ValidateSet("Cluster", "Node", "Vm", "Container")] + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, Container, or Group.")] + [ValidateSet("Cluster", "Node", "Vm", "Container", "Group")] public string Level { get; set; } = string.Empty; [Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")] @@ -21,6 +21,9 @@ namespace PSProxmoxVE.Cmdlets.Firewall [ValidateRange(100, 999999999)] public int? VmId { get; set; } + [Parameter(Mandatory = false, HelpMessage = "The security group name. Required when Level is Group.")] + public string? Group { get; set; } + [Parameter(Mandatory = true, HelpMessage = "The rule position to update.")] public int Position { get; set; } @@ -65,7 +68,8 @@ namespace PSProxmoxVE.Cmdlets.Firewall protected override void ProcessRecord() { var level = Level; - if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase) && + !string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(Node)) { @@ -86,8 +90,21 @@ namespace PSProxmoxVE.Cmdlets.Firewall return; } } + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(Group)) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Group is required when Level is Group."), + "GroupRequired", ErrorCategory.InvalidArgument, null)); + return; + } + } - if (!ShouldProcess($"firewall rule at position {Position} ({Level})", "Update")) + var target = string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase) + ? $"firewall rule at position {Position} ({Level} '{Group}')" + : $"firewall rule at position {Position} ({Level})"; + if (!ShouldProcess(target, "Update")) return; var session = GetSession(); @@ -122,7 +139,10 @@ namespace PSProxmoxVE.Cmdlets.Firewall config["iface"] = Iface!; WriteVerbose($"Updating firewall rule at position {Position} ({level})..."); - service.UpdateRule(session, level, Position, config, Node, vmid); + if (string.Equals(level, "Group", StringComparison.OrdinalIgnoreCase)) + service.UpdateGroupRule(session, Group!, Position, config); + else + service.UpdateRule(session, level, Position, config, Node, vmid); } } } diff --git a/tests/PSProxmoxVE.Core.Tests/Services/FirewallServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/FirewallServiceTests.cs new file mode 100644 index 0000000..62cfb5a --- /dev/null +++ b/tests/PSProxmoxVE.Core.Tests/Services/FirewallServiceTests.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using Moq; +using Xunit; +using PSProxmoxVE.Core.Authentication; +using PSProxmoxVE.Core.Client; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Core.Tests.Services +{ + public class FirewallServiceTests + { + private const string Group = "web-servers"; + + private sealed class CapturedPost + { + public int Calls { get; set; } + public string? Path { get; set; } + public Dictionary? Form { get; set; } + } + + private sealed class CapturedPut + { + public int Calls { get; set; } + public string? Path { get; set; } + public Dictionary? Form { get; set; } + } + + private static PveSession CreateSession() + { + return new PveSession("pve.example.com", 8006, false, + "root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); + } + + private static string RulesJson() => @"{ + ""data"": [ + { ""pos"": 0, ""type"": ""in"", ""action"": ""ACCEPT"", ""enable"": 1 }, + { ""pos"": 1, ""type"": ""out"", ""action"": ""DROP"", ""enable"": 0 } + ] + }"; + + private static CapturedPost CapturePost(Mock mockClient, string json) + { + var captured = new CapturedPost(); + mockClient.Setup(c => c.PostAsync(It.IsAny(), It.IsAny>())) + .Callback?>((path, form) => + { + captured.Calls++; + captured.Path = path; + captured.Form = form; + }) + .ReturnsAsync(json); + return captured; + } + + private static CapturedPut CapturePut(Mock mockClient, string json) + { + var captured = new CapturedPut(); + mockClient.Setup(c => c.PutAsync(It.IsAny(), It.IsAny>())) + .Callback?>((path, form) => + { + captured.Calls++; + captured.Path = path; + captured.Form = form; + }) + .ReturnsAsync(json); + return captured; + } + + // ------------------------------------------------------------------------- + // GetGroupRules + // ------------------------------------------------------------------------- + + [Fact] + public void GetGroupRules_ReturnsRuleArray() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync(It.IsAny())).ReturnsAsync(RulesJson()); + + var service = new FirewallService(mockClient.Object); + + var rules = service.GetGroupRules(CreateSession(), Group); + + Assert.Equal(2, rules.Length); + Assert.Equal(0, rules[0].Pos); + Assert.Equal("in", rules[0].Type); + Assert.Equal("ACCEPT", rules[0].Action); + Assert.Equal(1, rules[1].Pos); + Assert.Equal("out", rules[1].Type); + Assert.Equal("DROP", rules[1].Action); + + mockClient.Verify(c => c.GetAsync($"cluster/firewall/groups/{Group}"), Times.Once); + } + + [Fact] + public void GetGroupRules_NullData_ReturnsEmptyArray() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync(It.IsAny())).ReturnsAsync(@"{""data"": null}"); + + var service = new FirewallService(mockClient.Object); + + var rules = service.GetGroupRules(CreateSession(), Group); + + Assert.Empty(rules); + } + + [Fact] + public void GetGroupRules_EscapesGroupInPath() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync(It.IsAny())).ReturnsAsync(RulesJson()); + + var service = new FirewallService(mockClient.Object); + + service.GetGroupRules(CreateSession(), "web servers"); + + mockClient.Verify(c => c.GetAsync("cluster/firewall/groups/web%20servers"), Times.Once); + } + + // ------------------------------------------------------------------------- + // CreateGroupRule + // ------------------------------------------------------------------------- + + [Fact] + public void CreateGroupRule_PostsExactPathAndForm() + { + var mockClient = new Mock(); + var captured = CapturePost(mockClient, @"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + var config = new Dictionary + { + ["type"] = "in", + ["action"] = "ACCEPT", + ["enable"] = "1", + ["source"] = "10.0.0.0/8" + }; + + service.CreateGroupRule(CreateSession(), Group, config); + + Assert.Equal(1, captured.Calls); + Assert.Equal($"cluster/firewall/groups/{Group}", captured.Path); + Assert.NotNull(captured.Form); + Assert.Equal("in", captured.Form!["type"]); + Assert.Equal("ACCEPT", captured.Form["action"]); + Assert.Equal("1", captured.Form["enable"]); + Assert.Equal("10.0.0.0/8", captured.Form["source"]); + Assert.Equal(4, captured.Form.Count); + } + + [Fact] + public void CreateGroupRule_MinimalForm_ForwardsOnlyProvidedKeys() + { + var mockClient = new Mock(); + var captured = CapturePost(mockClient, @"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + service.CreateGroupRule(CreateSession(), Group, new Dictionary + { + ["type"] = "in", + ["action"] = "ACCEPT" + }); + + Assert.NotNull(captured.Form); + Assert.False(captured.Form!.ContainsKey("enable")); + Assert.False(captured.Form.ContainsKey("comment")); + Assert.Equal(2, captured.Form.Count); + } + + [Fact] + public void CreateGroupRule_EscapesGroupInPath() + { + var mockClient = new Mock(); + var captured = CapturePost(mockClient, @"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + service.CreateGroupRule(CreateSession(), "web servers", new Dictionary + { + ["type"] = "in", + ["action"] = "ACCEPT" + }); + + Assert.Equal("cluster/firewall/groups/web%20servers", captured.Path); + } + + // ------------------------------------------------------------------------- + // UpdateGroupRule + // ------------------------------------------------------------------------- + + [Fact] + public void UpdateGroupRule_PutsExactPathAndForm() + { + var mockClient = new Mock(); + var captured = CapturePut(mockClient, @"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + var config = new Dictionary + { + ["action"] = "DROP", + ["comment"] = "tightened" + }; + + service.UpdateGroupRule(CreateSession(), Group, 2, config); + + Assert.Equal(1, captured.Calls); + Assert.Equal($"cluster/firewall/groups/{Group}/2", captured.Path); + Assert.NotNull(captured.Form); + Assert.Equal("DROP", captured.Form!["action"]); + Assert.Equal("tightened", captured.Form["comment"]); + Assert.Equal(2, captured.Form.Count); + } + + [Fact] + public void UpdateGroupRule_EscapesGroupInPath() + { + var mockClient = new Mock(); + var captured = CapturePut(mockClient, @"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + service.UpdateGroupRule(CreateSession(), "web servers", 0, new Dictionary + { + ["action"] = "DROP" + }); + + Assert.Equal("cluster/firewall/groups/web%20servers/0", captured.Path); + } + + // ------------------------------------------------------------------------- + // RemoveGroupRule + // ------------------------------------------------------------------------- + + [Fact] + public void RemoveGroupRule_CallsDeleteAsync_ExactPath() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.DeleteAsync(It.IsAny())).ReturnsAsync(@"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + service.RemoveGroupRule(CreateSession(), Group, 3); + + mockClient.Verify(c => c.DeleteAsync($"cluster/firewall/groups/{Group}/3"), Times.Once); + mockClient.VerifyNoOtherCalls(); + } + + [Fact] + public void RemoveGroupRule_EscapesGroupInPath() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.DeleteAsync(It.IsAny())).ReturnsAsync(@"{""data"": null}"); + var service = new FirewallService(mockClient.Object); + + service.RemoveGroupRule(CreateSession(), "web servers", 1); + + mockClient.Verify(c => c.DeleteAsync("cluster/firewall/groups/web%20servers/1"), Times.Once); + } + + // ------------------------------------------------------------------------- + // Guard clauses + // ------------------------------------------------------------------------- + + [Fact] + public void GetGroupRules_NullSession_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("session", () => service.GetGroupRules(null!, Group)); + } + + [Fact] + public void GetGroupRules_WhitespaceGroup_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("group", () => service.GetGroupRules(CreateSession(), " ")); + } + + [Fact] + public void CreateGroupRule_NullSession_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("session", + () => service.CreateGroupRule(null!, Group, new Dictionary())); + } + + [Fact] + public void CreateGroupRule_WhitespaceGroup_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("group", + () => service.CreateGroupRule(CreateSession(), " ", new Dictionary())); + } + + [Fact] + public void UpdateGroupRule_NullSession_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("session", + () => service.UpdateGroupRule(null!, Group, 0, new Dictionary())); + } + + [Fact] + public void UpdateGroupRule_WhitespaceGroup_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("group", + () => service.UpdateGroupRule(CreateSession(), " ", 0, new Dictionary())); + } + + [Fact] + public void RemoveGroupRule_NullSession_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("session", () => service.RemoveGroupRule(null!, Group, 0)); + } + + [Fact] + public void RemoveGroupRule_WhitespaceGroup_ThrowsArgumentNullException() + { + var service = new FirewallService(new Mock().Object); + + Assert.Throws("group", () => service.RemoveGroupRule(CreateSession(), " ", 0)); + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Firewall/FirewallRuleCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Firewall/FirewallRuleCmdlets.Tests.ps1 index 400a4a5..dbe9450 100644 --- a/tests/PSProxmoxVE.Tests/Firewall/FirewallRuleCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Firewall/FirewallRuleCmdlets.Tests.ps1 @@ -86,6 +86,21 @@ Describe 'Get-PveFirewallRule' { Should -Throw '*No active Proxmox VE session*' } } + + Context 'Group level' { + It 'Should include Group in the Level ValidateSet' { + Skip-IfMissing 'Get-PveFirewallRule' + $param = $script:Cmd.Parameters['Level'] + $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } + $validateSet.ValidValues | Should -Contain 'Group' + } + + It 'Should throw when Level is Group and Group is not specified' { + Skip-IfMissing 'Get-PveFirewallRule' + { Get-PveFirewallRule -Level 'Group' -ErrorAction Stop } | + Should -Throw '*Group is required when Level is Group*' + } + } } # --------------------------------------------------------------------------- @@ -138,6 +153,21 @@ Describe 'New-PveFirewallRule' { Should -Throw '*No active Proxmox VE session*' } } + + Context 'Group level' { + It 'Should include Group in the Level ValidateSet' { + Skip-IfMissing 'New-PveFirewallRule' + $param = $script:Cmd.Parameters['Level'] + $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } + $validateSet.ValidValues | Should -Contain 'Group' + } + + It 'Should throw when Level is Group and Group is not specified' { + Skip-IfMissing 'New-PveFirewallRule' + { New-PveFirewallRule -Level 'Group' -Action 'ACCEPT' -Type 'in' -ErrorAction Stop } | + Should -Throw '*Group is required when Level is Group*' + } + } } # --------------------------------------------------------------------------- @@ -185,6 +215,21 @@ Describe 'Set-PveFirewallRule' { Should -Throw '*No active Proxmox VE session*' } } + + Context 'Group level' { + It 'Should include Group in the Level ValidateSet' { + Skip-IfMissing 'Set-PveFirewallRule' + $param = $script:Cmd.Parameters['Level'] + $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } + $validateSet.ValidValues | Should -Contain 'Group' + } + + It 'Should throw when Level is Group and Group is not specified' { + Skip-IfMissing 'Set-PveFirewallRule' + { Set-PveFirewallRule -Level 'Group' -Position 0 -ErrorAction Stop } | + Should -Throw '*Group is required when Level is Group*' + } + } } # --------------------------------------------------------------------------- @@ -240,4 +285,19 @@ Describe 'Remove-PveFirewallRule' { Should -Throw '*No active Proxmox VE session*' } } + + Context 'Group level' { + It 'Should include Group in the Level ValidateSet' { + Skip-IfMissing 'Remove-PveFirewallRule' + $param = $script:Cmd.Parameters['Level'] + $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } + $validateSet.ValidValues | Should -Contain 'Group' + } + + It 'Should throw when Level is Group and Group is not specified' { + Skip-IfMissing 'Remove-PveFirewallRule' + { Remove-PveFirewallRule -Level 'Group' -Position 0 -Confirm:$false -ErrorAction Stop } | + Should -Throw '*Group is required when Level is Group*' + } + } }