diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f4cf9..0eb2723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## Added +### 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. +- 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 [issue #47](https://github.com/dsccommunity/DscResource.Common/issues/47). diff --git a/README.md b/README.md index 987ad17..9c9f8f0 100644 --- a/README.md +++ b/README.md @@ -674,8 +674,9 @@ A new design pattern was introduces that uses the cmdlet [`Compare-ResourcePrope ```plaintext Test-DscParameterState [-CurrentValues] [-DesiredValues] - [[-ValuesToCheck] ] [-TurnOffTypeChecking] [-ReverseCheck] - [-SortArrayValues] [] + [-Properties] [[-ExcludeProperties] ] + [-TurnOffTypeChecking] [-ReverseCheck] [-SortArrayValues] + [] ``` @@ -711,12 +712,27 @@ $getTargetResourceParameters = @{ $returnValue = Test-DscParameterState ` -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` -DesiredValues $PSBoundParameters ` - -ValuesToCheck @( + -ExcludeProperties @( 'FailsafeOperator' 'NotificationMethod' ) ``` +##### Example 3 + +```powershell +$getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name +} + +$returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name +``` + This compares the values in the current state against the desires state. The function `Get-TargetResource` is called using just the required parameters to get the values in the current state. diff --git a/source/Public/Test-DscParameterState.ps1 b/source/Public/Test-DscParameterState.ps1 index 6386dd0..7fbfc89 100644 --- a/source/Public/Test-DscParameterState.ps1 +++ b/source/Public/Test-DscParameterState.ps1 @@ -14,7 +14,11 @@ The hashtable of desired values. For example $PSBoundParameters with the desired values. - .PARAMETER ValuesToCheck + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties This is a list of which properties in the desired values list should be checked. If this is empty then all values in DesiredValues are checked. @@ -48,14 +52,33 @@ $returnValue = Test-DscParameterState ` -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` -DesiredValues $PSBoundParameters ` - -ValuesToCheck @( + -ExcludeProperties @( 'FailsafeOperator' 'NotificationMethod' ) This compares the values in the current state against the desires state. The function Get-TargetResource is called using just the required parameters - to get the values in the current state. + to get the values in the current state. The parameter 'ExcludeProperties' + is used to exclude the properties 'FailsafeOperator' and + 'NotificationMethod' from the comparison. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The 'Properties' parameter is used + to to only compare the properties 'ServerName' and 'Name'. #> function Test-DscParameterState { @@ -72,7 +95,12 @@ function Test-DscParameterState [Parameter()] [System.String[]] - $ValuesToCheck, + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, [Parameter()] [System.Management.Automation.SwitchParameter] @@ -117,22 +145,26 @@ function Test-DscParameterState -ArgumentName 'CurrentValues' } - if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $ValuesToCheck) + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) { New-InvalidArgumentException ` - -Message $script:localizedData.InvalidValuesToCheckError ` - -ArgumentName 'ValuesToCheck' + -Message $script:localizedData.InvalidPropertiesError ` + -ArgumentName Properties } $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues - if (-not $ValuesToCheck) + if (-not $Properties) { $keyList = $desiredValuesClean.Keys } else { - $keyList = $ValuesToCheck + $keyList = $Properties + } + if ($ExcludeProperties) + { + $keyList = $keyList | Where-Object -FilterScript { $_ -notin $ExcludeProperties } } foreach ($key in $keyList) @@ -151,7 +183,7 @@ function Test-DscParameterState $currentValue = ConvertTo-HashTable -CimInstance $currentValue } - if ($null -ne $desiredValue) + if ($desiredValue) { $desiredType = $desiredValue.GetType() } @@ -162,7 +194,7 @@ function Test-DscParameterState } } - if ($null -ne $currentValue) + if ($currentValue) { $currentType = $currentValue.GetType() } @@ -260,13 +292,13 @@ function Test-DscParameterState if ($SortArrayValues) { - $desiredArrayValues = $desiredArrayValues | Sort-Object - $currentArrayValues = $currentArrayValues | Sort-Object + $desiredArrayValues = @($desiredArrayValues | Sort-Object) + $currentArrayValues = @($currentArrayValues | Sort-Object) } for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) { - if ($null -ne $desiredArrayValues[$i]) + if ($desiredArrayValues[$i]) { $desiredType = $desiredArrayValues[$i].GetType() } @@ -277,7 +309,7 @@ function Test-DscParameterState } } - if ($null -ne $currentArrayValues[$i]) + if ($currentArrayValues[$i]) { $currentType = $currentArrayValues[$i].GetType() } @@ -299,6 +331,49 @@ function Test-DscParameterState } } + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentArrayValuesConverted = $false + if ($currentArrayValues[$i] -is [scriptblock]) + { + $currentArrayValues[$i] = if ($desiredArrayValues[$i] -is [string]) + { + $currentArrayValues[$i] = $currentArrayValues[$i].Invoke() + } + else + { + $currentArrayValues[$i].ToString() + } + $wasCurrentArrayValuesConverted = $true + } + if ($desiredArrayValues[$i] -is [scriptblock]) + { + $desiredArrayValues[$i] = if ($currentArrayValues[$i] -is [string] -and -not $wasCurrentArrayValuesConverted) + { + $desiredArrayValues[$i].Invoke() + } + else + { + $desiredArrayValues[$i].ToString() + } + } + + if ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentArrayValues[$i] + $param.DesiredValues = $desiredArrayValues[$i] + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) { Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) @@ -319,7 +394,6 @@ function Test-DscParameterState $param = $PSBoundParameters $param.CurrentValues = $currentValue $param.DesiredValues = $desiredValue - $null = $param.Remove('ValuesToCheck') if ($returnValue) { @@ -333,6 +407,32 @@ function Test-DscParameterState } else { + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentValue = $false + if ($currentValue -is [scriptblock]) + { + $currentValue = if ($desiredValue -is [string]) + { + $currentValue = $currentValue.Invoke() + } + else + { + $currentValue.ToString() + } + $wasCurrentValue = $true + } + if ($desiredValue -is [scriptblock]) + { + $desiredValue = if ($currentValue -is [string] -and -not $wasCurrentValue) + { + $desiredValue.Invoke() + } + else + { + $desiredValue.ToString() + } + } + if ($desiredValue -ne $currentValue) { Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) @@ -360,6 +460,5 @@ function Test-DscParameterState } Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) - return $returnValue } diff --git a/source/en-US/DscResource.Common.strings.psd1 b/source/en-US/DscResource.Common.strings.psd1 index 99250ac..fd8440b 100644 --- a/source/en-US/DscResource.Common.strings.psd1 +++ b/source/en-US/DscResource.Common.strings.psd1 @@ -9,7 +9,7 @@ ConvertFrom-StringData @' AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) - InvalidValuesToCheckError = If 'DesiredValues' is a CimInstance then property 'ValuesToCheck' must contain a value. (DRC0016) + InvalidPropertiesError = If 'DesiredValues' is a CimInstance then property 'Properties' must contain a value. (DRC0016) MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) diff --git a/tests/Unit/Private/Test-DscObjectHasProperty.Tests.ps1 b/tests/Unit/Private/Test-DscObjectHasProperty.Tests.ps1 index 908422a..19f9f62 100644 --- a/tests/Unit/Private/Test-DscObjectHasProperty.Tests.ps1 +++ b/tests/Unit/Private/Test-DscObjectHasProperty.Tests.ps1 @@ -8,7 +8,8 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ Import-Module $ProjectName InModuleScope $ProjectName { - Describe 'NetworkingDsc.Common\Test-DscObjectHasProperty' { + Describe 'Test-DscObjectHasProperty' { + # Use the Get-Verb cmdlet to just get a simple object fast $testDscObject = (Get-Verb)[0] diff --git a/tests/Unit/Public/Test-DscParameterState.Tests.ps1 b/tests/Unit/Public/Test-DscParameterState.Tests.ps1 index 7c7394a..b4db119 100644 --- a/tests/Unit/Public/Test-DscParameterState.Tests.ps1 +++ b/tests/Unit/Public/Test-DscParameterState.Tests.ps1 @@ -19,28 +19,30 @@ InModuleScope $ProjectName { Context 'When testing single values' { $currentValues = @{ - String = 'a string' - Bool = $true - Int = 99 - Array = 'a', 'b', 'c' - Hashtable = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ k1 = 'Test' k2 = 123 k3 = 'v1', 'v2', 'v3' } + ScriptBlock = { Get-Date } } Context 'When all values match' { $desiredValues = [PSObject] @{ - String = 'a string' - Bool = $true - Int = 99 - Array = 'a', 'b', 'c' - Hashtable = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ k1 = 'Test' k2 = 123 k3 = 'v1', 'v2', 'v3' } + ScriptBlock = { Get-Date } } It 'Should not throw exception' { @@ -130,6 +132,57 @@ InModuleScope $ProjectName { } } + Context 'When an scriptblock is mismatched' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + ScriptBlock = { Get-Process } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse + } + } + + Context 'When an int is mismatched' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse + } + } + Context 'When a type is mismatched' { $desiredValues = [PSObject] @{ String = 'a string' @@ -171,15 +224,21 @@ InModuleScope $ProjectName { } } - Context 'When a value is mismatched but valuesToCheck is used to exclude them' { - $desiredValues = [PSObject] @{ - String = 'a string' - Bool = $false - Int = 1 - Array = @( 'a', 'b' ) + Context 'When a value is mismatched but ExcludeProperties is used to exclude then' { + $desiredValues = @{ + String = 'some other string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + ScriptBlock = { Get-Date } } - $valuesToCheck = @( + $excludeProperties = @( 'String' ) @@ -187,13 +246,82 @@ InModuleScope $ProjectName { { $script:result = Test-DscParameterState ` -CurrentValues $currentValues ` -DesiredValues $desiredValues ` - -ValuesToCheck $valuesToCheck ` + -ExcludeProperties $excludeProperties ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $true' { + $script:result | Should -BeTrue + } + } + + Context 'When a value is mismatched but it is not in Properties then' { + $desiredValues = @{ + String = 'some other string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + ScriptBlock = { Get-Date } + } + + $properties = @( + 'Bool' + 'Int' + 'Array' + 'Hashtable' + 'ScriptBlock' + ) + + Context 'When using the alias ValuesToCheck' { + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -ValuesToCheck $properties ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $true' { + $script:result | Should -BeTrue + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Properties $properties ` -Verbose:$verbose } | Should -Not -Throw } It 'Should return $true' { $script:result | Should -BeTrue } + + $properties = @( + 'String' + 'Bool' + 'Int' + 'Array' + 'Hashtable' + ) + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Properties $properties ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse + } } }