Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
'<value could not be serialized>'
}
}

function Test-ParameterFilter {
[CmdletBinding()]
param (
Expand Down Expand Up @@ -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"})."
}

Expand Down Expand Up @@ -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'
Expand Down
24 changes: 24 additions & 0 deletions tst/functions/Mock.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down