From 5360d9c2520a52ddf0c92226f37d65d53f6eb215 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 18:55:11 +0200 Subject: [PATCH 01/13] `Assert-BoundParameter`: Add support for 'AtLeastOne' parameter set to validate at least one parameter is bound --- CHANGELOG.md | 4 ++ source/Public/Assert-BoundParameter.ps1 | 39 +++++++++++- source/en-US/DscResource.Common.strings.psd1 | 3 + .../Public/Assert-BoundParameter.Tests.ps1 | 61 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33dae38..24ae747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `Assert-BoundParameter` + - Added parameter set `AtLeastOne` with parameter `AtLeastOneList` to + validate that at least one parameter from a specified list is bound + [#161](https://github.com/dsccommunity/DscResource.Common/issues/161). - `Format-Path` - Added parameter `ExpandEnvironmentVariable` fixes [#147](https://github.com/dsccommunity/DscResource.Common/issues/147). - Added support to `Compare-DscParameterState` for comparing large hashtables diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index 5a7c4c5..6c6c4a1 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION This command asserts passed parameters. It takes a hashtable, normally - `$PSBoundParameters`. There are two parameter sets for this command. + `$PSBoundParameters`. There are three parameter sets for this command. >There is no built in logic to validate against parameters sets for DSC >so this can be used instead to validate the parameters that were set in @@ -23,6 +23,11 @@ if not. Optionally it can be specified that parameters are only required if a specific parameter has been passed. + **AtLeastOne** + + Assert that at least one parameter from the specified list has been bound, + and throws an exception if none are present. + .PARAMETER BoundParameterList The parameters that should be evaluated against the mutually exclusive lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is @@ -47,6 +52,9 @@ If neither of the parameter names has been specified the evaluation of required parameters are not made. + .PARAMETER AtLeastOneList + An array of parameter names where at least one must be bound. + .EXAMPLE $assertBoundParameterParameters = @{ BoundParameterList = $PSBoundParameters @@ -77,6 +85,11 @@ Assert-BoundParameter -BoundParameterList $PSBoundParameters -RequiredParameter @('PBStartPortRange', 'PBEndPortRange') -RequiredBehavior 'AtLeastOnce' Throws an exception if at least one of the two parameters are not specified. + + .EXAMPLE + Assert-BoundParameter -BoundParameterList $PSBoundParameters -AtLeastOneList @('Severity', 'MessageId') + + Throws an exception if none of the parameters 'Severity' or 'MessageId' are specified. #> function Assert-BoundParameter { @@ -106,7 +119,11 @@ function Assert-BoundParameter [Parameter(ParameterSetName = 'RequiredParameter')] [System.String[]] - $IfParameterPresent + $IfParameterPresent, + + [Parameter(ParameterSetName = 'AtLeastOne', Mandatory = $true)] + [System.String[]] + $AtLeastOneList ) switch ($PSCmdlet.ParameterSetName) @@ -122,7 +139,7 @@ function Assert-BoundParameter $script:localizedData.ParameterUsageWrong ` -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") - New-InvalidArgumentException -ArgumentName 'Parameters' -Message $errorMessage + New-ArgumentException -ArgumentName 'Parameters' -Message $errorMessage } break @@ -138,5 +155,21 @@ function Assert-BoundParameter Assert-RequiredCommandParameter @PSBoundParameters break } + + 'AtLeastOne' + { + $boundParametersFromList = $BoundParameterList.Keys.Where({ $_ -in $AtLeastOneList }) + + if ($boundParametersFromList.Count -eq 0) + { + $errorMessage = ` + $script:localizedData.Assert_BoundParameter_AtLeastOneParameterMustBeSet ` + -f ($AtLeastOneList -join "','") + + New-ArgumentException -ArgumentName 'Parameters' -Message $errorMessage + } + + break + } } } diff --git a/source/en-US/DscResource.Common.strings.psd1 b/source/en-US/DscResource.Common.strings.psd1 index 73c6767..1b1b1af 100644 --- a/source/en-US/DscResource.Common.strings.psd1 +++ b/source/en-US/DscResource.Common.strings.psd1 @@ -40,6 +40,9 @@ ConvertFrom-StringData @' ## Assert-ElevatedUser ElevatedUser_UserNotElevated = This command must run in an elevated PowerShell session. (DRC0043) + ## Assert-BoundParameter + Assert_BoundParameter_AtLeastOneParameterMustBeSet = At least one of the parameters '{0}' must be specified. (DRC0052) + ## Assert-RequiredCommandParameter RequiredCommandParameter_SpecificParametersMustAllBeSet = The parameters '{0}' must all be specified. (DRC0044) RequiredCommandParameter_SpecificParametersMustAllBeSetWhenParameterExist = The parameters '{0}' must all be specified if either parameter '{1}' is specified. (DRC0045) diff --git a/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 b/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 index 7d300cb..23ebeb8 100644 --- a/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 +++ b/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 @@ -57,6 +57,11 @@ Describe 'Assert-BoundParameter' -Tag 'AssertBoundParameter' { # cSpell: disable-next MockExpectedParameters = '-BoundParameterList -RequiredParameter [-RequiredBehavior ] [-IfParameterPresent ] []' } + @{ + MockParameterSetName = 'AtLeastOne' + # cSpell: disable-next + MockExpectedParameters = '-BoundParameterList -AtLeastOneList []' + } ) { InModuleScope -Parameters $_ -ScriptBlock { $result = (Get-Command -Name 'Assert-BoundParameter').ParameterSets | @@ -186,6 +191,41 @@ Describe 'Assert-BoundParameter' -Tag 'AssertBoundParameter' { } } } + + Context 'When using the AtLeastOne parameter set' { + Context 'When at least one parameter from the list is bound' { + It 'Should not throw an error' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'Warning' + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When multiple parameters from the list are bound' { + It 'Should not throw an error' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'Warning' + MessageId = '12345' + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } } Context 'When the assert fails' { @@ -237,6 +277,27 @@ Describe 'Assert-BoundParameter' -Tag 'AssertBoundParameter' { } } + Context 'When using the AtLeastOne parameter set and none of the required parameters are bound' { + It 'Should throw an error' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Assert_BoundParameter_AtLeastOneParameterMustBeSet + } + + $errorMessage = $errorMessage -f "Severity','MessageId" + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw -ExpectedMessage "$errorMessage*" + } + } + Context 'When the required parameter is not present' { BeforeAll { Mock -CommandName Assert-RequiredCommandParameter -MockWith { From aa03430e58730e289498131e3d5898385ec86242 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 18:55:22 +0200 Subject: [PATCH 02/13] Replace New-InvalidArgumentException with New-ArgumentException for improved error handling in Assert-IPAddress, Compare-DscParameterState, and Find-Certificate functions. --- source/Public/Assert-IPAddress.ps1 | 6 +++--- source/Public/Compare-DscParameterState.ps1 | 6 +++--- source/Public/Find-Certificate.ps1 | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/Public/Assert-IPAddress.ps1 b/source/Public/Assert-IPAddress.ps1 index 32b8f4a..d0d4a77 100644 --- a/source/Public/Assert-IPAddress.ps1 +++ b/source/Public/Assert-IPAddress.ps1 @@ -53,7 +53,7 @@ function Assert-IPAddress if (-not ([System.Net.IPAddress]::TryParse($Address, [ref] $ipAddress))) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.AddressFormatError -f $Address) ` -ArgumentName 'Address' } @@ -66,7 +66,7 @@ function Assert-IPAddress { if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.AddressIPv6MismatchError -f $Address, $AddressFamily) ` -ArgumentName 'AddressFamily' } @@ -76,7 +76,7 @@ function Assert-IPAddress { if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.AddressIPv4MismatchError -f $Address, $AddressFamily) ` -ArgumentName 'AddressFamily' } diff --git a/source/Public/Compare-DscParameterState.ps1 b/source/Public/Compare-DscParameterState.ps1 index d933f53..9dd41b6 100644 --- a/source/Public/Compare-DscParameterState.ps1 +++ b/source/Public/Compare-DscParameterState.ps1 @@ -206,14 +206,14 @@ function Compare-DscParameterState if ($DesiredValues.GetType().FullName -notin $types) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` -ArgumentName 'DesiredValues' } if ($CurrentValues.GetType().FullName -notin $types) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` -ArgumentName 'CurrentValues' } @@ -221,7 +221,7 @@ function Compare-DscParameterState #region check if CimInstance and not have properties in parameters invoke exception if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) { - New-InvalidArgumentException ` + New-ArgumentException ` -Message $script:localizedData.InvalidPropertiesError ` -ArgumentName Properties } diff --git a/source/Public/Find-Certificate.ps1 b/source/Public/Find-Certificate.ps1 index 136207d..48b0c7c 100644 --- a/source/Public/Find-Certificate.ps1 +++ b/source/Public/Find-Certificate.ps1 @@ -125,7 +125,7 @@ function Find-Certificate if (-not (Test-Path -Path $certPath)) { # The Certificte Path is not valid - New-InvalidArgumentException ` + New-ArgumentException ` -Message ($script:localizedData.CertificatePathError -f $certPath) ` -ArgumentName 'Store' } # if From 6d03e507d2d1453636e36d72bee2e7fec3038979 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:03:14 +0200 Subject: [PATCH 03/13] Update task configurations in tasks.json to include necessary arguments for build, test, and hqrmtest tasks --- .vscode/tasks.json | 61 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2991140..011f8d0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,7 +41,7 @@ "label": "build", "type": "shell", "command": "&${cwd}/build.ps1", - "args": [], + "args": ["-AutoRestore", "-UseModuleFast", "-Tasks", "build"], "presentation": { "echo": true, "reveal": "always", @@ -84,7 +84,64 @@ "label": "test", "type": "shell", "command": "&${cwd}/build.ps1", - "args": ["-AutoRestore","-Tasks","test"], + "args": [ + "-AutoRestore", + "-UseModuleFast", + "-Tasks", + "test", + "-PesterPath", + "./tests/Unit/Classes,./tests/Unit/Private,./tests/Unit/Public,./tests/QA", + "-CodeCoverageThreshold", + "0" + ], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + { + "owner": "powershell", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": [ + { + "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", + "message": 1 + }, + { + "regexp": "(.*)", + "code": 1 + }, + { + "regexp": "" + }, + { + "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", + "file": 1, + "line": 2 + } + ] + } + ] + }, + { + "label": "hqrmtest", + "type": "shell", + "command": "&${cwd}/build.ps1", + "args": [ + "-AutoRestore", + "-UseModuleFast", + "-Tasks", + "hqrmtest", + "-CodeCoverageThreshold", + "0" + ], "presentation": { "echo": true, "reveal": "always", From 7da2b20332cfd30f8873a7789c1cb5ab3e7c1e4f Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:05:17 +0200 Subject: [PATCH 04/13] Add integration tests for Set-DscMachineRebootRequired and Set-PSModulePath commands --- .../{Public => Commands}/Set-DscMachineRebootRequired.Tests.ps1 | 0 tests/Integration/{Public => Commands}/Set-PSModulePath.Tests.ps1 | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/Integration/{Public => Commands}/Set-DscMachineRebootRequired.Tests.ps1 (100%) rename tests/Integration/{Public => Commands}/Set-PSModulePath.Tests.ps1 (100%) diff --git a/tests/Integration/Public/Set-DscMachineRebootRequired.Tests.ps1 b/tests/Integration/Commands/Set-DscMachineRebootRequired.Tests.ps1 similarity index 100% rename from tests/Integration/Public/Set-DscMachineRebootRequired.Tests.ps1 rename to tests/Integration/Commands/Set-DscMachineRebootRequired.Tests.ps1 diff --git a/tests/Integration/Public/Set-PSModulePath.Tests.ps1 b/tests/Integration/Commands/Set-PSModulePath.Tests.ps1 similarity index 100% rename from tests/Integration/Public/Set-PSModulePath.Tests.ps1 rename to tests/Integration/Commands/Set-PSModulePath.Tests.ps1 From 0f8e032e1affa7ed26da8ab1c27bb59f96420b37 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:05:21 +0200 Subject: [PATCH 05/13] Add integration tests for Assert-BoundParameter command to validate parameter binding scenarios --- ...ssert-BoundParameter.Integration.Tests.ps1 | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 diff --git a/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 b/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 new file mode 100644 index 0000000..c147afe --- /dev/null +++ b/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 @@ -0,0 +1,245 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'DscResource.Common' + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' +} + +Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterIntegration' { + Context 'When using MutuallyExclusiveParameters parameter set' { + Context 'When no conflicting parameters are bound' { + It 'Should not throw when no parameters are bound' { + { + Assert-BoundParameter -BoundParameterList @{} -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when only parameters from the first list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when only parameters from the second list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Param2 = 'Value2' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when parameters from neither list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Param3 = 'Value3' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + } | Should -Not -Throw + } + } + + Context 'When conflicting parameters are bound' { + It 'Should throw when parameters from both lists are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + } | Should -Throw + } + + It 'Should throw when multiple parameters from both lists are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param1b = 'Value1b'; Param2 = 'Value2'; Param2b = 'Value2b' } -MutuallyExclusiveList1 @('Param1', 'Param1b') -MutuallyExclusiveList2 @('Param2', 'Param2b') -ErrorAction Stop + } | Should -Throw + } + } + } + + Context 'When using RequiredParameter parameter set' { + Context 'When required parameters are bound' { + It 'Should not throw when all required parameters are bound with default behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when all required parameters are bound with explicit All behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior All -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when at least one required parameter is bound with Any behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when all required parameters are bound with Any behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + } | Should -Not -Throw + } + } + + Context 'When required parameters are not bound' { + It 'Should throw when not all required parameters are bound with default behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -ErrorAction Stop + } | Should -Throw + } + + It 'Should throw when not all required parameters are bound with explicit All behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior All -ErrorAction Stop + } | Should -Throw + } + + It 'Should throw when no required parameters are bound with Any behavior' { + { + Assert-BoundParameter -BoundParameterList @{ Param3 = 'Value3' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + } | Should -Throw + } + } + + Context 'When using IfParameterPresent condition' { + It 'Should not throw when condition parameter is not present' { + { + Assert-BoundParameter -BoundParameterList @{ OtherParam = 'Value' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when condition parameter is present and required parameters are bound' { + { + Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue'; Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should throw when condition parameter is present but required parameters are not bound' { + { + Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + } | Should -Throw + } + + It 'Should not throw when condition parameter is present and at least one required parameter is bound with Any behavior' { + { + Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue'; Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -IfParameterPresent @('TriggerParam') -ErrorAction Stop + } | Should -Not -Throw + } + } + } + + Context 'When using AtLeastOne parameter set' { + Context 'When at least one parameter from the list is bound' { + It 'Should not throw when one parameter from the list is bound' { + { + Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when multiple parameters from the list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; MessageId = '12345'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when all parameters from the list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; MessageId = '12345'; Level = 'Info' } -AtLeastOneList @('Severity', 'MessageId', 'Level') -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should not throw when only the last parameter from the list is bound' { + { + Assert-BoundParameter -BoundParameterList @{ MessageId = '12345'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Not -Throw + } + } + + Context 'When no parameters from the list are bound' { + It 'Should throw when no parameters from the list are bound' { + { + Assert-BoundParameter -BoundParameterList @{ OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Throw + } + + It 'Should throw when no parameters are bound at all' { + { + Assert-BoundParameter -BoundParameterList @{} -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Throw + } + + It 'Should throw when bound parameters do not match any in the list' { + { + Assert-BoundParameter -BoundParameterList @{ UnrelatedParam1 = 'Value1'; UnrelatedParam2 = 'Value2' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + } | Should -Throw + } + } + } + + Context 'When using complex real-world scenarios' { + Context 'When simulating DSC resource parameter validation' { + It 'Should validate SQL Server connection parameters correctly' { + # Simulate a scenario where either ServerInstance or ConnectionString must be provided + { + Assert-BoundParameter -BoundParameterList @{ ServerInstance = 'localhost'; Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ ConnectionString = 'Server=localhost;Database=TestDB'; Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + } | Should -Throw + } + + It 'Should validate certificate parameters correctly' { + # Simulate a scenario where certificate can be specified by Thumbprint or Subject, but not both + { + Assert-BoundParameter -BoundParameterList @{ Thumbprint = 'ABC123'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ Subject = 'CN=Test'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ Thumbprint = 'ABC123'; Subject = 'CN=Test'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + } | Should -Throw + } + + It 'Should validate conditional parameters correctly' { + # Simulate a scenario where if EnableSsl is specified, then CertificateThumbprint is required + { + Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost'; EnableSsl = $true; CertificateThumbprint = 'ABC123' } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost' } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + } | Should -Not -Throw + + { + Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost'; EnableSsl = $true } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + } | Should -Throw + } + } + } +} From 8d4cf0ee6899ffb75a4318c259dc0d8649adfe1b Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:28:41 +0200 Subject: [PATCH 06/13] Add additional words to cSpell configuration for improved spell checking --- .vscode/settings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c524097..686ef49 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,7 +32,10 @@ "keepachangelog", "notin", "pscmdlet", - "steppable" + "steppable", + "hashtables", + "HQRM", + "LCID" ], "[markdown]": { "files.trimTrailingWhitespace": true, From 9f0985f59556179c21afe1602ee822f336489aa7 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:28:48 +0200 Subject: [PATCH 07/13] Update CHANGELOG.md to document new parameters and fixes for DscResource.Common --- CHANGELOG.md | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ae747..a962bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added parameter set `AtLeastOne` with parameter `AtLeastOneList` to validate that at least one parameter from a specified list is bound [#161](https://github.com/dsccommunity/DscResource.Common/issues/161). + - Added parameter `IfEqualParameterList` to conditionally perform assertions + only when specified parameters have exact values [#160](https://github.com/dsccommunity/DscResource.Common/issues/160). - `Format-Path` - Added parameter `ExpandEnvironmentVariable` fixes [#147](https://github.com/dsccommunity/DscResource.Common/issues/147). - Added support to `Compare-DscParameterState` for comparing large hashtables @@ -326,8 +328,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - DscResource.Common - - updating the Get-LocalizedData to bypass Import-LocalizedData when in Globalization-Invariant mode. - The command throws when running on an Invariant culture on Linux in the latest PS versions. + - updating the Get-LocalizedData to bypass Import-LocalizedData when in + Globalization-Invariant mode. + The command throws when running on an Invariant culture on Linux in + the latest PS versions. ## [0.11.0] - 2022-08-01 @@ -341,9 +345,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Correction to `Compare-DscParameterState` returning false positive when parameter - with an empty hashtable or CimInstance property is passed in `DesriedValues` - fixes - [issue #65](https://github.com/dsccommunity/DscResource.Common/issues/65). -- Correction somes problems in `Compare-DscParameterState` - see [issue #70](https://github.com/dsccommunity/DscResource.Common/issues/70) : + with an empty hashtable or CimInstance property is passed in `DesiredValues`, + fixes [issue #65](https://github.com/dsccommunity/DscResource.Common/issues/65). +- Correction some problems in `Compare-DscParameterState`, see + [issue #70](https://github.com/dsccommunity/DscResource.Common/issues/70). - When you use `-ReverseCheck`, this value is used in recursive call of `Test-DscParameterState` and `Compare-DscParameterState`, and that called another time the function. @@ -402,15 +407,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Get-TargetResource function or Get() method in Class based Resources. It is based on the code of Test-DscParameterState function to get compliance between current and desired state of resources. - The OutPut of Compare-DscParameterState is a collection psobject. - The properties of psobject are Property,InDesiredState,ExpectedType,ActualType, - ExpectedValue and ActualValue. The IncludeInDesiredState parameter must be use to - add ExeptedValue and ActualValue. -- Added pester test to test the pscredential object with `Compare-DscParameterState`. + The OutPut of Compare-DscParameterState is a collection PSObject. + The properties of PSObject are Property,InDesiredState,ExpectedType,ActualType, + ExpectedValue and ActualValue. The IncludeInDesiredState parameter must + be use to add ExpectedValue and ActualValue. +- Added pester test to test the PSCredential object with `Compare-DscParameterState`. ### Changed -- Cmdlet Test-DscResourceState is now calling Compare-DscParameterState. Possible breaking change. +- Cmdlet Test-DscResourceState is now calling Compare-DscParameterState. + Possible breaking change. - IncludeInDesiredState and IncludeValue parameters of Compare-DscParameterState are removed in splatting when Test-DscCompareState is called. @@ -423,15 +429,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Fixed - Correction to `Test-DscParameterState` returning false positive when parameter - with an empty array is passed in `DesriedValues` or `CurrentValues` - fixes + with an empty array is passed in `DesiredValues` or `CurrentValues` - fixes [issue #53](https://github.com/dsccommunity/DscResource.Common/issues/53). ## [0.9.2] - 2020-07-22 ### Added -- `Test-DscParameterState` can now handle scriptblocks. The parameter 'ValuesToCheck' was renamed to 'Properties' but an alias - was added so it is not a braking change. The parameter 'ExcludeProperties' was added. +- `Test-DscParameterState` can now handle script blocks. The parameter + 'ValuesToCheck' was renamed to 'Properties' but an alias + was added so it is not a braking change. The parameter 'ExcludeProperties' + was added. - Added a new test for the alias 'ValuesToCheck' pointing to 'Properties'. - Added cmdlet `Compare-ResourcePropertyState` that also introduces a new design pattern to evaluate properties in both _Test_ and _Set_ - fixes @@ -462,8 +470,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.0] - 2020-05-11 -- Added a default value of `en-US` to the `DefaultUICulture` parameter of the `Get-LocalizedData` function - [Issue #33](https://github.com/dsccommunity/DscResource.Common/issues/33). +- Added a default value of `en-US` to the `DefaultUICulture` parameter of + the `Get-LocalizedData` function [Issue #33](https://github.com/dsccommunity/DscResource.Common/issues/33). - Fixing a problem with the latest ModuleBuild 1.7.0 that breaks the CI pipeline. ## [0.7.1] - 2020-05-02 @@ -528,7 +536,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed the New-*Exception function unit tests to work correctly on PowerShell version 5, 6 and 7. +- Fixed the New-*Exception function unit tests to work correctly on PowerShell + version 5, 6 and 7. ## [0.4.0] - 2020-03-09 From 86fbe9d7373b1b78dc0b7e8d62e3ac6a2d7d1830 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:32:13 +0200 Subject: [PATCH 08/13] Add IfEqualParameterList support to Assert-BoundParameter and corresponding tests --- source/Public/Assert-BoundParameter.ps1 | 59 ++++ .../Public/Assert-BoundParameter.Tests.ps1 | 271 +++++++++++++++++- 2 files changed, 327 insertions(+), 3 deletions(-) diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index 6c6c4a1..d6e2efa 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -52,6 +52,11 @@ If neither of the parameter names has been specified the evaluation of required parameters are not made. + .PARAMETER IfEqualParameterList + A hashtable of parameter names and their expected values. The assertion will + only be performed if all the specified parameters in the BoundParameterList + have the exact values specified in this hashtable. + .PARAMETER AtLeastOneList An array of parameter names where at least one must be bound. @@ -90,6 +95,37 @@ Assert-BoundParameter -BoundParameterList $PSBoundParameters -AtLeastOneList @('Severity', 'MessageId') Throws an exception if none of the parameters 'Severity' or 'MessageId' are specified. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Severity' + ) + MutuallyExclusiveList2 = @( + 'MessageId' + ) + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Severity` and `MessageId` and the parameter `Ensure` has + the value `Present`. + + .EXAMPLE + Assert-BoundParameter -BoundParameterList $PSBoundParameters -RequiredParameter @('Property2', 'Property3') -IfEqualParameterList @{ Property1 = 'SpecificValue' } + + Throws an exception if the parameter 'Property1' has the value 'SpecificValue' + and either of the required parameters are not specified. + + .EXAMPLE + Assert-BoundParameter -BoundParameterList $PSBoundParameters -AtLeastOneList @('Severity', 'MessageId') -IfEqualParameterList @{ Ensure = 'Present' } + + Throws an exception if the parameter 'Ensure' has the value 'Present' and + none of the parameters 'Severity' or 'MessageId' are specified. #> function Assert-BoundParameter { @@ -121,11 +157,29 @@ function Assert-BoundParameter [System.String[]] $IfParameterPresent, + [Parameter(ParameterSetName = 'MutuallyExclusiveParameters')] + [Parameter(ParameterSetName = 'RequiredParameter')] + [Parameter(ParameterSetName = 'AtLeastOne')] + [System.Collections.Hashtable] + $IfEqualParameterList, + [Parameter(ParameterSetName = 'AtLeastOne', Mandatory = $true)] [System.String[]] $AtLeastOneList ) + # Early return if IfEqualParameterList conditions are not met + if ($PSBoundParameters.ContainsKey('IfEqualParameterList')) + { + foreach ($parameterName in $IfEqualParameterList.Keys) + { + if (-not $BoundParameterList.ContainsKey($parameterName) -or $BoundParameterList[$parameterName] -ne $IfEqualParameterList[$parameterName]) + { + return + } + } + } + switch ($PSCmdlet.ParameterSetName) { 'MutuallyExclusiveParameters' @@ -147,6 +201,11 @@ function Assert-BoundParameter 'RequiredParameter' { + if ($PSBoundParameters.ContainsKey('IfEqualParameterList')) + { + $PSBoundParameters.Remove('IfEqualParameterList') + } + if (-not $PSBoundParameters.ContainsKey('RequiredBehavior')) { $PSBoundParameters.RequiredBehavior = $RequiredBehavior diff --git a/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 b/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 index 23ebeb8..b34f572 100644 --- a/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 +++ b/tests/Unit/Public/Assert-BoundParameter.Tests.ps1 @@ -50,17 +50,17 @@ Describe 'Assert-BoundParameter' -Tag 'AssertBoundParameter' { @{ MockParameterSetName = 'MutuallyExclusiveParameters' # cSpell: disable-next - MockExpectedParameters = '-BoundParameterList -MutuallyExclusiveList1 -MutuallyExclusiveList2 []' + MockExpectedParameters = '-BoundParameterList -MutuallyExclusiveList1 -MutuallyExclusiveList2 [-IfEqualParameterList ] []' } @{ MockParameterSetName = 'RequiredParameter' # cSpell: disable-next - MockExpectedParameters = '-BoundParameterList -RequiredParameter [-RequiredBehavior ] [-IfParameterPresent ] []' + MockExpectedParameters = '-BoundParameterList -RequiredParameter [-RequiredBehavior ] [-IfParameterPresent ] [-IfEqualParameterList ] []' } @{ MockParameterSetName = 'AtLeastOne' # cSpell: disable-next - MockExpectedParameters = '-BoundParameterList -AtLeastOneList []' + MockExpectedParameters = '-BoundParameterList -AtLeastOneList [-IfEqualParameterList ] []' } ) { InModuleScope -Parameters $_ -ScriptBlock { @@ -313,4 +313,269 @@ Describe 'Assert-BoundParameter' -Tag 'AssertBoundParameter' { } } } + + Context 'When using IfEqualParameterList parameter' { + Context 'When using MutuallyExclusiveParameters parameter set' { + Context 'When the IfEqualParameterList condition is met' { + It 'Should throw an error when mutually exclusive parameters are both present' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.ParameterUsageWrong + } + + $errorMessage = $errorMessage -f "Severity", "MessageId" + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + MessageId = '12345' + Ensure = 'Present' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw -ExpectedMessage "$errorMessage*" + } + + It 'Should not throw an error when mutually exclusive parameters are not both present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + Ensure = 'Present' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not throw an error even when mutually exclusive parameters are both present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + MessageId = '12345' + Ensure = 'Absent' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw an error when the parameter in IfEqualParameterList is not present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + MessageId = '12345' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When multiple conditions in IfEqualParameterList must match' { + It 'Should throw an error when all conditions are met and mutually exclusive parameters are present' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.ParameterUsageWrong + } + + $errorMessage = $errorMessage -f "Severity", "MessageId" + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + MessageId = '12345' + Ensure = 'Present' + Type = 'Server' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + Type = 'Server' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw -ExpectedMessage "$errorMessage*" + } + + It 'Should not throw an error when only some conditions are met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + MessageId = '12345' + Ensure = 'Present' + Type = 'Client' + } + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + Type = 'Server' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } + + Context 'When using RequiredParameter parameter set' { + BeforeAll { + Mock -CommandName Assert-RequiredCommandParameter + } + + Context 'When the IfEqualParameterList condition is met' { + It 'Should call Assert-RequiredCommandParameter when condition is met' { + InModuleScope -ScriptBlock { + $testParams = @{ + BoundParameterList = @{ + Property1 = 'SpecificValue' + Property2 = 'Value2' + } + RequiredParameter = @('Property2', 'Property3') + IfEqualParameterList = @{ + Property1 = 'SpecificValue' + } + } + + Assert-BoundParameter @testParams + } + + Should -Invoke -CommandName Assert-RequiredCommandParameter -Exactly -Times 1 -Scope It + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not call Assert-RequiredCommandParameter when condition is not met' { + InModuleScope -ScriptBlock { + $testParams = @{ + BoundParameterList = @{ + Property1 = 'DifferentValue' + Property2 = 'Value2' + } + RequiredParameter = @('Property2', 'Property3') + IfEqualParameterList = @{ + Property1 = 'SpecificValue' + } + } + + Assert-BoundParameter @testParams + } + + Should -Invoke -CommandName Assert-RequiredCommandParameter -Exactly -Times 0 -Scope It + } + } + } + + Context 'When using AtLeastOne parameter set' { + Context 'When the IfEqualParameterList condition is met' { + It 'Should throw an error when none of the required parameters are present' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Assert_BoundParameter_AtLeastOneParameterMustBeSet + } + + $errorMessage = $errorMessage -f "Severity','MessageId" + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Ensure = 'Present' + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw -ExpectedMessage "$errorMessage*" + } + + It 'Should not throw an error when at least one required parameter is present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'High' + Ensure = 'Present' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not throw an error even when none of the required parameters are present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Ensure = 'Absent' + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw an error when the parameter in IfEqualParameterList is not present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + OtherParam = 'value' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } + } } From 1e5918568fe255df8948d5ab186aefc8ee239c0c Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:50:21 +0200 Subject: [PATCH 09/13] Add integration tests for IfEqualParameterList in Assert-BoundParameter command --- ...ssert-BoundParameter.Integration.Tests.ps1 | 793 +++++++++++++++++- 1 file changed, 760 insertions(+), 33 deletions(-) diff --git a/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 b/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 index c147afe..450c913 100644 --- a/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Assert-BoundParameter.Integration.Tests.ps1 @@ -34,25 +34,53 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When no conflicting parameters are bound' { It 'Should not throw when no parameters are bound' { { - Assert-BoundParameter -BoundParameterList @{} -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{} + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when only parameters from the first list are bound' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param1 = 'Value1' } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when only parameters from the second list are bound' { { - Assert-BoundParameter -BoundParameterList @{ Param2 = 'Value2' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param2 = 'Value2' } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when parameters from neither list are bound' { { - Assert-BoundParameter -BoundParameterList @{ Param3 = 'Value3' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param3 = 'Value3' } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } } @@ -60,13 +88,35 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When conflicting parameters are bound' { It 'Should throw when parameters from both lists are bound' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -MutuallyExclusiveList1 @('Param1') -MutuallyExclusiveList2 @('Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should throw when multiple parameters from both lists are bound' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param1b = 'Value1b'; Param2 = 'Value2'; Param2b = 'Value2b' } -MutuallyExclusiveList1 @('Param1', 'Param1b') -MutuallyExclusiveList2 @('Param2', 'Param2b') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param1b = 'Value1b' + Param2 = 'Value2' + Param2b = 'Value2b' + } + MutuallyExclusiveList1 = @('Param1', 'Param1b') + MutuallyExclusiveList2 = @('Param2', 'Param2b') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } } @@ -76,25 +126,61 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When required parameters are bound' { It 'Should not throw when all required parameters are bound with default behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when all required parameters are bound with explicit All behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior All -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'All' + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when at least one required parameter is bound with Any behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param1 = 'Value1' } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'Any' + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when all required parameters are bound with Any behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'Any' + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } } @@ -102,19 +188,39 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When required parameters are not bound' { It 'Should throw when not all required parameters are bound with default behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param1 = 'Value1' } + RequiredParameter = @('Param1', 'Param2') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should throw when not all required parameters are bound with explicit All behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior All -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param1 = 'Value1' } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'All' + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should throw when no required parameters are bound with Any behavior' { { - Assert-BoundParameter -BoundParameterList @{ Param3 = 'Value3' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param3 = 'Value3' } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'Any' + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } } @@ -122,25 +228,61 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When using IfParameterPresent condition' { It 'Should not throw when condition parameter is not present' { { - Assert-BoundParameter -BoundParameterList @{ OtherParam = 'Value' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ OtherParam = 'Value' } + RequiredParameter = @('Param1', 'Param2') + IfParameterPresent = @('TriggerParam') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when condition parameter is present and required parameters are bound' { { - Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue'; Param1 = 'Value1'; Param2 = 'Value2' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + TriggerParam = 'TriggerValue' + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + IfParameterPresent = @('TriggerParam') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should throw when condition parameter is present but required parameters are not bound' { { - Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue' } -RequiredParameter @('Param1', 'Param2') -IfParameterPresent @('TriggerParam') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ TriggerParam = 'TriggerValue' } + RequiredParameter = @('Param1', 'Param2') + IfParameterPresent = @('TriggerParam') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should not throw when condition parameter is present and at least one required parameter is bound with Any behavior' { { - Assert-BoundParameter -BoundParameterList @{ TriggerParam = 'TriggerValue'; Param1 = 'Value1' } -RequiredParameter @('Param1', 'Param2') -RequiredBehavior Any -IfParameterPresent @('TriggerParam') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + TriggerParam = 'TriggerValue' + Param1 = 'Value1' + } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'Any' + IfParameterPresent = @('TriggerParam') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } } @@ -150,25 +292,57 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When at least one parameter from the list is bound' { It 'Should not throw when one parameter from the list is bound' { { - Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Severity = 'Warning'; OtherParam = 'Value' } + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when multiple parameters from the list are bound' { { - Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; MessageId = '12345'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'Warning' + MessageId = '12345' + OtherParam = 'Value' + } + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when all parameters from the list are bound' { { - Assert-BoundParameter -BoundParameterList @{ Severity = 'Warning'; MessageId = '12345'; Level = 'Info' } -AtLeastOneList @('Severity', 'MessageId', 'Level') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Severity = 'Warning' + MessageId = '12345' + Level = 'Info' + } + AtLeastOneList = @('Severity', 'MessageId', 'Level') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } It 'Should not throw when only the last parameter from the list is bound' { { - Assert-BoundParameter -BoundParameterList @{ MessageId = '12345'; OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ MessageId = '12345'; OtherParam = 'Value' } + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw } } @@ -176,19 +350,40 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt Context 'When no parameters from the list are bound' { It 'Should throw when no parameters from the list are bound' { { - Assert-BoundParameter -BoundParameterList @{ OtherParam = 'Value' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ OtherParam = 'Value' } + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should throw when no parameters are bound at all' { { - Assert-BoundParameter -BoundParameterList @{} -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{} + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should throw when bound parameters do not match any in the list' { { - Assert-BoundParameter -BoundParameterList @{ UnrelatedParam1 = 'Value1'; UnrelatedParam2 = 'Value2' } -AtLeastOneList @('Severity', 'MessageId') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + UnrelatedParam1 = 'Value1' + UnrelatedParam2 = 'Value2' + } + AtLeastOneList = @('Severity', 'MessageId') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } } @@ -199,45 +394,577 @@ Describe 'Assert-BoundParameter Integration Tests' -Tag 'AssertBoundParameterInt It 'Should validate SQL Server connection parameters correctly' { # Simulate a scenario where either ServerInstance or ConnectionString must be provided { - Assert-BoundParameter -BoundParameterList @{ ServerInstance = 'localhost'; Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ServerInstance = 'localhost' + Database = 'TestDB' + } + AtLeastOneList = @('ServerInstance', 'ConnectionString') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw { - Assert-BoundParameter -BoundParameterList @{ ConnectionString = 'Server=localhost;Database=TestDB'; Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConnectionString = 'Server=localhost;Database=TestDB' + Database = 'TestDB' + } + AtLeastOneList = @('ServerInstance', 'ConnectionString') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw { - Assert-BoundParameter -BoundParameterList @{ Database = 'TestDB' } -AtLeastOneList @('ServerInstance', 'ConnectionString') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Database = 'TestDB' } + AtLeastOneList = @('ServerInstance', 'ConnectionString') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should validate certificate parameters correctly' { # Simulate a scenario where certificate can be specified by Thumbprint or Subject, but not both { - Assert-BoundParameter -BoundParameterList @{ Thumbprint = 'ABC123'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Thumbprint = 'ABC123' + Store = 'My' + } + MutuallyExclusiveList1 = @('Thumbprint') + MutuallyExclusiveList2 = @('Subject') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw { - Assert-BoundParameter -BoundParameterList @{ Subject = 'CN=Test'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Subject = 'CN=Test' + Store = 'My' + } + MutuallyExclusiveList1 = @('Thumbprint') + MutuallyExclusiveList2 = @('Subject') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw { - Assert-BoundParameter -BoundParameterList @{ Thumbprint = 'ABC123'; Subject = 'CN=Test'; Store = 'My' } -MutuallyExclusiveList1 @('Thumbprint') -MutuallyExclusiveList2 @('Subject') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Thumbprint = 'ABC123' + Subject = 'CN=Test' + Store = 'My' + } + MutuallyExclusiveList1 = @('Thumbprint') + MutuallyExclusiveList2 = @('Subject') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } It 'Should validate conditional parameters correctly' { # Simulate a scenario where if EnableSsl is specified, then CertificateThumbprint is required { - Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost'; EnableSsl = $true; CertificateThumbprint = 'ABC123' } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ServerName = 'localhost' + EnableSsl = $true + CertificateThumbprint = 'ABC123' + } + RequiredParameter = @('CertificateThumbprint') + IfParameterPresent = @('EnableSsl') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ ServerName = 'localhost' } + RequiredParameter = @('CertificateThumbprint') + IfParameterPresent = @('EnableSsl') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ServerName = 'localhost' + EnableSsl = $true + } + RequiredParameter = @('CertificateThumbprint') + IfParameterPresent = @('EnableSsl') + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + } + } + + Context 'When using IfEqualParameterList parameter' { + Context 'When using with MutuallyExclusiveParameters parameter set' { + Context 'When the IfEqualParameterList condition is met' { + It 'Should throw when mutually exclusive parameters are both present and condition is met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + + It 'Should not throw when mutually exclusive parameters are not both present and condition is met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Param1 = 'Value1' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should throw when multiple conditions are met and mutually exclusive parameters are present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Production' + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Production' + } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not throw even when mutually exclusive parameters are both present but condition is not met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Basic' + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw when the parameter in IfEqualParameterList is not present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw when only some conditions are met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Development' + Param1 = 'Value1' + Param2 = 'Value2' + } + MutuallyExclusiveList1 = @('Param1') + MutuallyExclusiveList2 = @('Param2') + IfEqualParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Production' + } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } + + Context 'When using with RequiredParameter parameter set' { + Context 'When the IfEqualParameterList condition is met' { + It 'Should not throw when condition is met and required parameters are bound' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should throw when condition is met but required parameters are not bound' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Param1 = 'Value1' + } + RequiredParameter = @('Param1', 'Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + + It 'Should not throw when condition is met and at least one required parameter is bound with Any behavior' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Param1 = 'Value1' + } + RequiredParameter = @('Param1', 'Param2') + RequiredBehavior = 'Any' + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should work with multiple conditions and IfParameterPresent together' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Production' + TriggerParam = 'Present' + Param1 = 'Value1' + Param2 = 'Value2' + } + RequiredParameter = @('Param1', 'Param2') + IfParameterPresent = @('TriggerParam') + IfEqualParameterList = @{ + ConfigType = 'Advanced' + Environment = 'Production' + } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not throw even when required parameters are not bound but condition is not met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Basic' + Param1 = 'Value1' + } + RequiredParameter = @('Param1', 'Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw when the parameter in IfEqualParameterList is not present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ Param1 = 'Value1' } + RequiredParameter = @('Param1', 'Param2') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } + + Context 'When using with AtLeastOne parameter set' { + Context 'When the IfEqualParameterList condition is met' { + It 'Should not throw when condition is met and at least one required parameter is present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Severity = 'Warning' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should throw when condition is met but none of the required parameters are present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + OtherParam = 'Value' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + + It 'Should not throw when condition is met and multiple required parameters are present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Advanced' + Severity = 'Warning' + MessageId = '12345' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + + Context 'When the IfEqualParameterList condition is not met' { + It 'Should not throw even when none of the required parameters are present but condition is not met' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConfigType = 'Basic' + OtherParam = 'Value' + } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + + It 'Should not throw when the parameter in IfEqualParameterList is not present' { + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ OtherParam = 'Value' } + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ ConfigType = 'Advanced' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + } + } + } + + Context 'When using real-world DSC resource scenarios with conditional validation' { + It 'Should validate SQL Server authentication parameters only when authentication type is specified' { + # Should not validate credentials when using Windows Authentication + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + AuthenticationType = 'Windows' + ServerInstance = 'localhost' + } + RequiredParameter = @('Username', 'Password') + IfEqualParameterList = @{ AuthenticationType = 'SQL' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + + # Should validate credentials when using SQL Authentication and they are provided + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + AuthenticationType = 'SQL' + ServerInstance = 'localhost' + Username = 'sa' + Password = 'password' + } + RequiredParameter = @('Username', 'Password') + IfEqualParameterList = @{ AuthenticationType = 'SQL' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + + # Should throw when using SQL Authentication but credentials are missing + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + AuthenticationType = 'SQL' + ServerInstance = 'localhost' + } + RequiredParameter = @('Username', 'Password') + IfEqualParameterList = @{ AuthenticationType = 'SQL' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + + It 'Should validate certificate parameters only for secure connections' { + # Should not require certificate when connection is not secure + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConnectionType = 'Standard' + ServerName = 'localhost' + } + AtLeastOneList = @('CertificateThumbprint', 'CertificateSubject') + IfEqualParameterList = @{ ConnectionType = 'Secure' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw + # Should require certificate parameters when connection is secure and they are provided { - Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost' } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConnectionType = 'Secure' + ServerName = 'localhost' + CertificateThumbprint = 'ABC123' + } + AtLeastOneList = @('CertificateThumbprint', 'CertificateSubject') + IfEqualParameterList = @{ ConnectionType = 'Secure' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Not -Throw + # Should throw when connection is secure but no certificate parameters are provided { - Assert-BoundParameter -BoundParameterList @{ ServerName = 'localhost'; EnableSsl = $true } -RequiredParameter @('CertificateThumbprint') -IfParameterPresent @('EnableSsl') -ErrorAction Stop + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + ConnectionType = 'Secure' + ServerName = 'localhost' + } + AtLeastOneList = @('CertificateThumbprint', 'CertificateSubject') + IfEqualParameterList = @{ ConnectionType = 'Secure' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Throw + } + + It 'Should validate environment-specific parameters correctly' { + # Should allow conflicting parameters in development but not in production + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Environment = 'Development' + DebugMode = $true + OptimizedMode = $true + } + MutuallyExclusiveList1 = @('DebugMode') + MutuallyExclusiveList2 = @('OptimizedMode') + IfEqualParameterList = @{ Environment = 'Production' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters + } | Should -Not -Throw + + # Should throw when conflicting parameters are used in production + { + $assertBoundParameterParameters = @{ + BoundParameterList = @{ + Environment = 'Production' + DebugMode = $true + OptimizedMode = $true + } + MutuallyExclusiveList1 = @('DebugMode') + MutuallyExclusiveList2 = @('OptimizedMode') + IfEqualParameterList = @{ Environment = 'Production' } + ErrorAction = 'Stop' + } + + Assert-BoundParameter @assertBoundParameterParameters } | Should -Throw } } From d167b0f637a62e8b4dec18267ac7ebf34cba0ef3 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:12:44 +0200 Subject: [PATCH 10/13] Refactor error message assignment in Assert-BoundParameter function for clarity --- source/Public/Assert-BoundParameter.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index d6e2efa..4a17f47 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -221,9 +221,7 @@ function Assert-BoundParameter if ($boundParametersFromList.Count -eq 0) { - $errorMessage = ` - $script:localizedData.Assert_BoundParameter_AtLeastOneParameterMustBeSet ` - -f ($AtLeastOneList -join "','") + $errorMessage = $script:localizedData.Assert_BoundParameter_AtLeastOneParameterMustBeSet -f ($AtLeastOneList -join "','") New-ArgumentException -ArgumentName 'Parameters' -Message $errorMessage } From ffc29a87796b3ff90b1f75ea41285ba1c25f5235 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:12:47 +0200 Subject: [PATCH 11/13] Update examples in Assert-BoundParameter documentation for clarity on RequiredBehavior --- source/Public/Assert-BoundParameter.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index 4a17f47..ee92c31 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -87,9 +87,14 @@ of the required parameters are not. .EXAMPLE - Assert-BoundParameter -BoundParameterList $PSBoundParameters -RequiredParameter @('PBStartPortRange', 'PBEndPortRange') -RequiredBehavior 'AtLeastOnce' + Assert-BoundParameter -BoundParameterList $PSBoundParameters -RequiredParameter @('PBStartPortRange', 'PBEndPortRange') -RequiredBehavior 'Any' - Throws an exception if at least one of the two parameters are not specified. + Throws an exception if any of the two parameters are not present. + + .EXAMPLE + Assert-BoundParameter -BoundParameterList $PSBoundParameters -RequiredParameter @('PBStartPortRange', 'PBEndPortRange') -RequiredBehavior 'All' + + Throws an exception if all of the specified parameters are not present. .EXAMPLE Assert-BoundParameter -BoundParameterList $PSBoundParameters -AtLeastOneList @('Severity', 'MessageId') From 0d1775e91df9a1753d5f0fd17e89ae46b499e3b0 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:15:04 +0200 Subject: [PATCH 12/13] Fix grammar and clarity in Compare-DscParameterState documentation --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a962bb1..8118a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -407,10 +407,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Get-TargetResource function or Get() method in Class based Resources. It is based on the code of Test-DscParameterState function to get compliance between current and desired state of resources. - The OutPut of Compare-DscParameterState is a collection PSObject. - The properties of PSObject are Property,InDesiredState,ExpectedType,ActualType, - ExpectedValue and ActualValue. The IncludeInDesiredState parameter must - be use to add ExpectedValue and ActualValue. + The OutPut of Compare-DscParameterState is a collection of PSObject. + The properties of each PSObject are Property, InDesiredState, ExpectedType, ActualType, + ExpectedValue, and ActualValue. The IncludeInDesiredState parameter must + be used to add ExpectedValue and ActualValue. - Added pester test to test the PSCredential object with `Compare-DscParameterState`. ### Changed From 3aa298b3a620d5c5e0fbdb07589419b408d2b0bf Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:18:09 +0200 Subject: [PATCH 13/13] Refactor error message assignment in Assert-BoundParameter for improved readability --- source/Public/Assert-BoundParameter.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index ee92c31..280323d 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -194,9 +194,10 @@ function Assert-BoundParameter if ($itemFoundFromList1.Count -gt 0 -and $itemFoundFromList2.Count -gt 0) { - $errorMessage = ` - $script:localizedData.ParameterUsageWrong ` - -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") + $errorMessage = $script:localizedData.ParameterUsageWrong -f ( + ($MutuallyExclusiveList1 -join "','"), + ($MutuallyExclusiveList2 -join "','") + ) New-ArgumentException -ArgumentName 'Parameters' -Message $errorMessage }