Skip to content

Commit

Permalink
Fix mocks with enum-parameters and ValidateRange (#2191)
Browse files Browse the repository at this point in the history
* fix mocking for enum-param with validaterange

* update test to fix multiple parameters

* rename function and fix test

* improve test

* cleanup

* add cmdlet test on windows

* add support for untyped parameters

* cleanup

* fix test

* fix ValidateRangeKind test
  • Loading branch information
fflaten authored Jun 30, 2022
1 parent ed6135b commit dd7d09e
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function Create-MockHook ($contextInfo, $InvokeMockCallback) {

$metadata = Repair-ConflictingParameters -Metadata $metadata -RemoveParameterType $RemoveParameterType -RemoveParameterValidation $RemoveParameterValidation
$paramBlock = [Management.Automation.ProxyCommand]::GetParamBlock($metadata)
$paramBlock = Repair-EnumParameters -ParamBlock $paramBlock -Metadata $metadata

if ($contextInfo.Command.CommandType -eq 'Cmdlet') {
$dynamicParamBlock = "dynamicparam { & `$MyInvocation.MyCommand.Mock.Get_MockDynamicParameter -CmdletName '$($contextInfo.Command.Name)' -Parameters `$PSBoundParameters }"
Expand Down Expand Up @@ -1805,3 +1806,55 @@ function New-BlockWithoutParameterAliases {
$PSCmdlet.ThrowTerminatingError($_)
}
}

function Repair-EnumParameters {
param (
[string]
$ParamBlock,
[System.Management.Automation.CommandMetadata]
$Metadata
)

# proxycommand breaks ValidateRange for enum-parameters
# broken arguments (unquoted strings) will show as NamedArguments in ast, while valid arguments are PositionalArguments.
# https://github.com/pester/Pester/issues/1496
# https://github.com/PowerShell/PowerShell/issues/17546
$ast = [System.Management.Automation.Language.Parser]::ParseInput("param($ParamBlock)", [ref]$null, [ref]$null)
$brokenValidateRange = $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.AttributeAst] -and
$node.TypeName.Name -match '(?:ValidateRange|System\.Management\.Automation\.ValidateRangeAttribute)$' -and
$node.NamedArguments.Count -gt 0 -and
# triple checking for broken argument - it won't have a value/expression
$node.NamedArguments.ExpressionOmitted -notcontains $false
}, $false)

if ($brokenValidateRange.Count -eq 0) {
# No errors found. Return original string
return $ParamBlock
}

$sb = & $SafeCommands['New-Object'] System.Text.StringBuilder($ParamBlock)

foreach ($attr in $brokenValidateRange) {
$paramName = $attr.Parent.Name.VariablePath.UserPath
$originalAttribute = $Metadata.Parameters[$paramName].Attributes | & $SafeCommands['Where-Object'] { $_ -is [ValidateRange] }
$enumType = @($originalAttribute)[0].MinRange.GetType()
if (-not $enumType.IsEnum) { continue }

# prefix arguments with [My.Enum.Type]::
$enumPrefix = "[$($enumType.FullName)]::"
$fixedValidation = $attr.Extent.Text -replace '(\w+)(?=,\s|\)\])', "$enumPrefix`$1"

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock -Message "Fixed ValidateRange-attribute parameter '$paramName' from '$($attr.Extent.Text)' to '$fixedValidation'"
}

# make sure we modify the correct parameter by modifying the whole thing
$orgParameter = $attr.Parent.Extent.Text
$fixedParameter = $orgParameter.Replace($attr.Extent.Text, $fixedValidation)
$null = $sb.Replace($orgParameter, $fixedParameter)
}

$sb.ToString()
}
66 changes: 66 additions & 0 deletions tst/functions/Mock.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2751,6 +2751,72 @@ Describe 'RemoveParameterValidation' {
}
}

Describe 'Mocking command with ValidateRange-attributes' {
# https://github.com/pester/Pester/issues/1496
# https://github.com/PowerShell/PowerShell/issues/17546
# Bug in PowerShell. ProxyCommand-generation breaks ValidateRange-attributes for enum-parameters

It 'mocked function does not throw when param is <Name>' -TestCases @(
@{
# min and max are enum-values -> affected by bug, needs Repair-EnumParameters
Name = 'typed using enum min max'
Attribute = '[ValidateRange([Microsoft.PowerShell.ExecutionPolicy]::Unrestricted, [Microsoft.PowerShell.ExecutionPolicy]::Undefined)]'
Parameter = '[Microsoft.PowerShell.ExecutionPolicy]$TypedBroken'
},
@{
# min and max are enum-values -> affected by bug, needs Repair-EnumParameters
Name = 'untyped using enum min max'
Attribute = '[ValidateRange([Microsoft.PowerShell.ExecutionPolicy]::Unrestricted, [Microsoft.PowerShell.ExecutionPolicy]::Undefined)]'
Parameter = '$UntypedBroken'
},
@{
# min and max are enum-values -> affected by bug, needs Repair-EnumParameters. make sure regex didn't match partial (Clear)
Name = 'untyped using enum min max with similar valuenames'
Attribute = '[ValidateRange([System.ConsoleKey]::Clear, [System.ConsoleKey]::OemClear)]'
Parameter = '[Parameter()][System.ConsoleKey]$TypedBrokenWithSimilarAttributeArgNames'
},
@{
# int Min, enum Max -> Both are set as int in command metadata -> unaffected by bug
Name = 'typed using int min enum max'
Attribute = '[ValidateRange(0, [Microsoft.PowerShell.ExecutionPolicy]::Undefined)]'
Parameter = '[Microsoft.PowerShell.ExecutionPolicy]$Works'
},
@{
# enum Min, int Max -> Both are set as int in command metadata -> unaffected by bug
Name = 'typed using enum min max'
Attribute = '[ValidateRange([Microsoft.PowerShell.ExecutionPolicy]::Unrestricted, 0)]'
Parameter = '[Microsoft.PowerShell.ExecutionPolicy]$Works2'
}
) {
Set-Item -Path 'function:Test-EnumValidation' -Value ('param ( {0}{1} )' -f $Attribute, $Parameter)

Mock -CommandName 'Test-EnumValidation' -MockWith { 'mock' }
Test-EnumValidation | Should -Be 'mock'
}

if ($PSVersionTable.PSVersion.Major -ge '7') {
# ValidateRangeKind -> unaffected by bug but verify nothing broke
It 'mocked function does not throw when param is type using ValidateRangeKind' {
$Name = 'typed using RangeKind'
$Attribute = '[ValidateRange([System.Management.Automation.ValidateRangeKind]::Positive)]'
$Parameter = '[int]$Works2'

Set-Item -Path 'function:Test-EnumValidation' -Value ('param ( {0}{1} )' -f $Attribute, $Parameter)

Mock -CommandName 'Test-EnumValidation' -MockWith { 'mock' }
Test-EnumValidation | Should -Be 'mock'
}
}

# Only built-in cmdlet with affected parameters are Start/Set-BitsTransfer. Only available on Windows
if ((Get-Module BitsTransfer -ErrorAction SilentlyContinue)) {
It 'mocked cmdlet does not throw' {
Mock -CommandName 'Start-BitsTransfer' -MockWith { 'mock' }
Start-BitsTransfer -Source "/nonexistingpath" | Should -Be 'mock'
}
}
}

Describe "Running Mock with ModuleName in test scope" {
BeforeAll {
Get-Module "test" -ErrorAction SilentlyContinue | Remove-Module
Expand Down

0 comments on commit dd7d09e

Please sign in to comment.