diff --git a/src/functions/Mock.ps1 b/src/functions/Mock.ps1 index 95d129cd2..318a98923 100644 --- a/src/functions/Mock.ps1 +++ b/src/functions/Mock.ps1 @@ -1259,6 +1259,22 @@ function Invoke-InMockScope { } } +function Format-BoundParameterValueSafely { + # Stringify a bound-parameter value for diagnostic text (debug messages and the failed-filter + # summary). This is only ever used to provide context to the user, so it must fail open: some + # types have a ToString that throws (e.g. mocked SMO objects created via New-MockObject), and + # a value like that must not make the whole mock filter throw when it is not even referenced by + # the filter. See #2953. + param($Value) + + try { + "$Value" + } + catch { + '' + } +} + function Test-ParameterFilter { [CmdletBinding()] param ( @@ -1319,7 +1335,7 @@ function Test-ParameterFilter { if ($PesterPreference.Debug.WriteDebugMessages.Value) { $hasContext = 0 -lt $Context.Count - $c = $(if ($hasContext) { foreach ($p in $Context.GetEnumerator()) { "$($p.Key) = $($p.Value)" } }) -join ", " + $c = $(if ($hasContext) { foreach ($p in $Context.GetEnumerator()) { "$($p.Key) = $(Format-BoundParameterValueSafely $p.Value)" } }) -join ", " Write-PesterDebugMessage -Scope Mock -Message "Running mock filter { $scriptBlock } $(if ($hasContext) { "with context: $c" } else { "without any context"})." } @@ -1367,7 +1383,7 @@ function Test-ParameterFilter { $filterText = $scriptBlock.ToString().Trim() $hasContext = 0 -lt $Context.Count $contextText = if ($hasContext) { - 'bound parameters: ' + (($Context.GetEnumerator() | & $SafeCommands['ForEach-Object'] { "$($_.Key) = $($_.Value)" }) -join ', ') + 'bound parameters: ' + (($Context.GetEnumerator() | & $SafeCommands['ForEach-Object'] { "$($_.Key) = $(Format-BoundParameterValueSafely $_.Value)" }) -join ', ') } else { 'no bound parameters' diff --git a/tst/functions/Mock.Tests.ps1 b/tst/functions/Mock.Tests.ps1 index 3eff3c157..a8da14d40 100644 --- a/tst/functions/Mock.Tests.ps1 +++ b/tst/functions/Mock.Tests.ps1 @@ -337,6 +337,30 @@ Describe 'When calling Mock, StrictMode is enabled, and variables are used in th } } +Describe 'When a bound parameter value has a ToString that throws' { + # The parameter filter serializer only builds diagnostic text, so a value whose ToString throws + # (e.g. a mocked SMO type) must not make the mock throw when the value is not even referenced by + # the filter. See #2953. + BeforeAll { + function Get-Thing { + param ( + [object] $InputObject, + [switch] $Other + ) + } + } + + It 'Does not throw when a non-matching parameter filter is present' { + $throwingToString = [pscustomobject]@{ Name = 'demo' } + $throwingToString | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString should not be called by the parameter filter serializer' } -Force + + Mock Get-Thing { 'default' } + Mock Get-Thing -ParameterFilter { $Other.IsPresent } { 'other' } + + { Get-Thing -InputObject $throwingToString } | Should -Not -Throw + } +} + Describe "When calling Mock on existing function without matching bound params" { It "Should throw because no parameter filter matched the call" { Mock FunctionUnderTest { return "fake results" } -parameterFilter { $param1 -eq "test" }