Skip to content

Commit e4f9092

Browse files
fflatennohwnd
andauthored
Throw on unbound user-provided scriptblocks (#2551)
* Block unbound scriptblocks as input * Improve error message for unbound scriptblock input to clarify potential issues Co-authored-by: Jakub Jareš <[email protected]> --------- Co-authored-by: Jakub Jareš <[email protected]>
1 parent 65a2a31 commit e4f9092

19 files changed

+179
-4
lines changed

src/Main.ps1

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ function Add-ShouldOperator {
8080
[switch] $SupportsArrayInput
8181
)
8282

83+
Assert-BoundScriptBlockInput -ScriptBlock $Test
84+
8385
$entry = [PSCustomObject]@{
8486
Test = $Test
8587
SupportsArrayInput = [bool]$SupportsArrayInput
@@ -1260,6 +1262,8 @@ function BeforeDiscovery {
12601262
[ScriptBlock]$ScriptBlock
12611263
)
12621264

1265+
Assert-BoundScriptBlockInput -ScriptBlock $ScriptBlock
1266+
12631267
if ($ExecutionContext.SessionState.PSVariable.Get('invokedViaInvokePester')) {
12641268
if ($state.CurrentBlock.IsRoot -and -not $state.CurrentBlock.FrameworkData.MissingParametersProcessed) {
12651269
# For undefined parameters in container, add parameter's default value to Data

src/Pester.Runtime.ps1

+21
Original file line numberDiff line numberDiff line change
@@ -2474,6 +2474,10 @@ function New-BlockContainerObject {
24742474
default { throw [System.ArgumentOutOfRangeException]'' }
24752475
}
24762476

2477+
if ($item -is [scriptblock]) {
2478+
Assert-BoundScriptBlockInput -ScriptBlock $item
2479+
}
2480+
24772481
$c = [Pester.ContainerInfo]::Create()
24782482
$c.Type = $type
24792483
$c.Item = $item
@@ -2604,3 +2608,20 @@ function Add-MissingContainerParameters ($RootBlock, $Container, $CallingFunctio
26042608

26052609
$RootBlock.FrameworkData.MissingParametersProcessed = $true
26062610
}
2611+
2612+
function Assert-BoundScriptBlockInput {
2613+
param(
2614+
[Parameter(Mandatory = $true)]
2615+
[ScriptBlock] $ScriptBlock
2616+
)
2617+
$internalSessionState = $script:ScriptBlockSessionStateInternalProperty.GetValue($ScriptBlock, $null)
2618+
if ($null -eq $internalSessionState) {
2619+
$maxLength = 250
2620+
$prettySb = (Format-Nicely2 $ScriptBlock) -replace '\s{2,}', ' '
2621+
if ($prettySb.Length -gt $maxLength) {
2622+
$prettySb = "$($prettySb.Remove($maxLength))..."
2623+
}
2624+
2625+
throw [System.ArgumentException]::new("Unbound scriptblock is not allowed, because it would run inside of Pester session state and produce unexpected results. See https://github.com/pester/Pester/issues/2411 for more details and workarounds. ScriptBlock: '$prettySb'")
2626+
}
2627+
}

src/functions/Context.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
}
106106
}
107107

108+
Assert-BoundScriptBlockInput -ScriptBlock $Fixture
109+
108110
if ($ExecutionContext.SessionState.PSVariable.Get('invokedViaInvokePester')) {
109111
if ($state.CurrentBlock.IsRoot -and -not $state.CurrentBlock.FrameworkData.MissingParametersProcessed) {
110112
# For undefined parameters in container, add parameter's default value to Data

src/functions/Describe.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
}
114114
}
115115

116+
Assert-BoundScriptBlockInput -ScriptBlock $Fixture
117+
116118
if ($ExecutionContext.SessionState.PSVariable.Get('invokedViaInvokePester')) {
117119
if ($state.CurrentBlock.IsRoot -and -not $state.CurrentBlock.FrameworkData.MissingParametersProcessed) {
118120
# For undefined parameters in container, add parameter's default value to Data

src/functions/It.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@
152152
}
153153
}
154154

155+
Assert-BoundScriptBlockInput -ScriptBlock $Test
156+
155157
if ($PSBoundParameters.ContainsKey('ForEach')) {
156158
if ($null -eq $ForEach -or 0 -eq @($ForEach).Count) {
157159
if ($PesterPreference.Run.FailOnNullOrEmptyForEach.Value -and -not $AllowNullOrEmptyForEach) {

src/functions/SetupTeardown.ps1

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
$Scriptblock
5858
)
5959
Assert-DescribeInProgress -CommandName BeforeEach
60+
Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock
6061

6162
New-EachTestSetup -ScriptBlock $Scriptblock
6263
}
@@ -123,6 +124,7 @@ function AfterEach {
123124
$Scriptblock
124125
)
125126
Assert-DescribeInProgress -CommandName AfterEach
127+
Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock
126128

127129
New-EachTestTeardown -ScriptBlock $Scriptblock
128130
}
@@ -198,6 +200,7 @@ function BeforeAll {
198200
[Scriptblock]
199201
$Scriptblock
200202
)
203+
Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock
201204

202205
New-OneTimeTestSetup -ScriptBlock $Scriptblock
203206
}
@@ -265,6 +268,7 @@ function AfterAll {
265268
$Scriptblock
266269
)
267270
Assert-DescribeInProgress -CommandName AfterAll
271+
Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock
268272

269273
New-OneTimeTestTeardown -ScriptBlock $Scriptblock
270274
}

src/functions/assert/Collection/Should-All.ps1

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
[String]$Because
4646
)
4747

48+
Assert-BoundScriptBlockInput -ScriptBlock $FilterScript
4849

4950
$Expected = $FilterScript
5051
$collectedInput = Collect-Input -ParameterInput $Actual -PipelineInput $local:Input -IsPipelineInput $MyInvocation.ExpectingInput

src/functions/assert/Collection/Should-Any.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
[String]$Because
4545
)
4646

47+
Assert-BoundScriptBlockInput -ScriptBlock $FilterScript
48+
4749
$Expected = $FilterScript
4850
$collectedInput = Collect-Input -ParameterInput $Actual -PipelineInput $local:Input -IsPipelineInput $MyInvocation.ExpectingInput
4951
$Actual = $collectedInput.Actual

src/functions/assert/Exception/Should-Throw.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ function Should-Throw {
6767
$collectedInput = Collect-Input -ParameterInput $ScriptBlock -PipelineInput $local:Input -IsPipelineInput $MyInvocation.ExpectingInput -UnrollInput
6868
$ScriptBlock = $collectedInput.Actual
6969

70+
Assert-BoundScriptBlockInput -ScriptBlock $ScriptBlock
71+
7072
$errorThrown = $false
7173
$err = $null
7274
try {

tst/Pester.RSpec.ts.ps1

+44
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,12 @@ i -PassThru:$PassThru {
16681668
}
16691669
}
16701670
}
1671+
1672+
t "Does not accept unbound scriptblocks" {
1673+
# Would execute in Pester's internal module state
1674+
$ex = { New-PesterContainer -ScriptBlock ([ScriptBlock]::Create('$true')) } | Verify-Throw
1675+
$ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
1676+
}
16711677
}
16721678

16731679
b "BeforeDiscovery" {
@@ -1691,6 +1697,15 @@ i -PassThru:$PassThru {
16911697
$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
16921698
$r.Containers[0].Blocks[1].Tests[0].Result | Verify-Equal "Passed"
16931699
}
1700+
1701+
t "Does not accept unbound scriptblocks" {
1702+
# Would execute in Pester's internal module state
1703+
$sb = { BeforeDiscovery ([ScriptBlock]::Create('$true')) }
1704+
$container = New-PesterContainer -ScriptBlock $sb
1705+
$r = Invoke-Pester -Container $container -PassThru
1706+
$r.Containers[0].Result | Verify-Equal 'Failed'
1707+
$r.Containers[0].ErrorRecord.Exception.Message | Verify-Like 'Unbound scriptblock*'
1708+
}
16941709
}
16951710

16961711
b "Parametric tests" {
@@ -2922,4 +2937,33 @@ i -PassThru:$PassThru {
29222937
$pwd.Path | Verify-Equal $beforePWD
29232938
}
29242939
}
2940+
2941+
b 'Unbound scriptblocks' {
2942+
# Would execute in Pester's internal module state
2943+
t 'Throws when provided to Run.ScriptBlock' {
2944+
$sb = [scriptblock]::Create('')
2945+
$conf = New-PesterConfiguration
2946+
$conf.Run.ScriptBlock = $sb
2947+
$conf.Run.Throw = $true
2948+
$conf.Output.CIFormat = 'None'
2949+
2950+
$ex = { Invoke-Pester -Configuration $conf } | Verify-Throw
2951+
$ex.Exception.Message | Verify-Like '*Unbound scriptblock*'
2952+
}
2953+
2954+
t 'Throws when provided to Run.Container' {
2955+
$c = [Pester.ContainerInfo]::Create()
2956+
$c.Type = 'ScriptBlock'
2957+
$c.Item = [scriptblock]::Create('')
2958+
$c.Data = @{}
2959+
2960+
$conf = New-PesterConfiguration
2961+
$conf.Run.Container = $c
2962+
$conf.Run.Throw = $true
2963+
$conf.Output.CIFormat = 'None'
2964+
2965+
$ex = { Invoke-Pester -Configuration $conf } | Verify-Throw
2966+
$ex.Exception.Message | Verify-Like '*Unbound scriptblock*'
2967+
}
2968+
}
29252969
}

tst/functions/Add-ShouldOperator.ts.ps1

+11
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ i -PassThru:$PassThru {
7070
}
7171
}
7272

73+
b 'Add-ShouldOperator input validation' {
74+
Get-Module Pester | Remove-Module
75+
Import-Module "$PSScriptRoot\..\..\bin\Pester.psd1"
76+
77+
t 'Does not allow unbound scriptblocks' {
78+
# Would execute in Pester's internal module state
79+
$ex = { Add-ShouldOperator -Name DenyUnbound -Test ([ScriptBlock]::Create('$true')) } | Verify-Throw
80+
$ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
81+
}
82+
}
83+
7384
b 'Executing custom Should assertions' {
7485
# Testing paramter and output syntax described in docs (https://pester.dev/docs/assertions/custom-assertions)
7586
Get-Module Pester | Remove-Module

tst/functions/Context.Tests.ps1

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Describe 'Testing Context' {
77

88
}
99
}
10-
} | should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
10+
} | Should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
1111
}
1212

1313
It "Has a name that looks like a script block" {
@@ -17,6 +17,11 @@ Describe 'Testing Context' {
1717

1818
}
1919
}
20-
} | should -Throw 'No test fixture is provided. (Have you put the open curly brace on the next line?)'
20+
} | Should -Throw 'No test fixture is provided. (Have you put the open curly brace on the next line?)'
21+
}
22+
23+
It 'Throws when provided unbound scriptblock' {
24+
# Unbound scriptblocks would execute in Pester's internal module state
25+
{ Context 'c' -Fixture ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
2126
}
2227
}

tst/functions/Describe.Tests.ps1

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ Describe 'Testing Describe' {
3636
}
3737
} | Should -Throw 'Test fixture name has multiple lines and no test fixture is provided. (Have you provided a name for the test group?)'
3838
}
39+
40+
It 'Throws when provided unbound scriptblock' {
41+
# Unbound scriptblocks would execute in Pester's internal module state
42+
{ Describe 'd' -Fixture ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
43+
}
3944
}
4045

4146

tst/functions/It.Tests.ps1

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Set-StrictMode -Version Latest
2+
3+
Describe 'Testing It' {
4+
It 'Throws when missing name' {
5+
{ It {
6+
7+
'something'
8+
}
9+
} | Should -Throw -ExpectedMessage 'Test name has multiple lines and no test scriptblock is provided*'
10+
}
11+
12+
It 'Throws when missing scriptblock' {
13+
{ It 'runs a test'
14+
{
15+
# This scriptblock is a new statement as scriptblock didn't start on It-line nor used a backtick
16+
}
17+
} | Should -Throw -ExpectedMessage 'No test scriptblock is provided*'
18+
}
19+
20+
It 'Throws when provided unbound scriptblock' {
21+
# Unbound scriptblocks would execute in Pester's internal module state
22+
{ It 'i' -Test ([scriptblock]::Create('')) } | Should -Throw -ExpectedMessage 'Unbound scriptblock*'
23+
}
24+
}

tst/functions/Mock.Tests.ps1

+11-1
Original file line numberDiff line numberDiff line change
@@ -2366,15 +2366,25 @@ Describe 'Naming conflicts in mocked functions' {
23662366
}
23672367

23682368
Describe 'Passing unbound script blocks as mocks' {
2369-
It 'Does not produce an error' {
2369+
BeforeAll {
23702370
function TestMe {
23712371
'Original'
23722372
}
2373+
}
2374+
It 'Does not produce an error' {
23732375
$scriptBlock = [scriptblock]::Create('"Mocked"')
23742376

23752377
{ Mock TestMe $scriptBlock } | Should -Not -Throw
23762378
TestMe | Should -Be Mocked
23772379
}
2380+
2381+
It 'Should not execute in Pester internal state' {
2382+
$filter = [scriptblock]::Create('if ("pester" -eq $ExecutionContext.SessionState.Module) { throw "executed parameter filter in internal state" } else { $true }')
2383+
$scriptBlock = [scriptblock]::Create('if ("pester" -eq $ExecutionContext.SessionState.Module) { throw "executed mock in internal state" } else { "Mocked" }')
2384+
2385+
{ Mock -CommandName TestMe -ParameterFilter $filter -MockWith $scriptBlock } | Should -Not -Throw
2386+
TestMe -SomeParam | Should -Be Mocked
2387+
}
23782388
}
23792389

23802390
Describe 'Should -Invoke when mock called outside of It block' {

tst/functions/SetupTeardown.Tests.ps1

+19
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ Describe 'Finishing TestGroup Setup and Teardown tests' {
190190
}
191191
}
192192

193+
Describe 'Unbound scriptsblocks as input' {
194+
# Unbound scriptblocks would execute in Pester's internal module state
195+
BeforeAll {
196+
$sb = [scriptblock]::Create('')
197+
$expectedMessage = 'Unbound scriptblock*'
198+
}
199+
It 'Throws when provided to BeforeAll' {
200+
{ BeforeAll -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
201+
}
202+
It 'Throws when provided to AfterAll' {
203+
{ AfterAll -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
204+
}
205+
It 'Throws when provided to BeforeEach' {
206+
{ BeforeEach -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
207+
}
208+
It 'Throws when provided to AfterEach' {
209+
{ AfterEach -Scriptblock $sb } | Should -Throw -ExpectedMessage $expectedMessage
210+
}
211+
}
193212

194213
# if ($PSVersionTable.PSVersion.Major -ge 3) {
195214
# # TODO: this depends on the old pester internals it would be easier to test in P

tst/functions/assert/Collection/Should-All.Tests.ps1

+6
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ Expected [int] 2, but got [int] 1." -replace "`r`n", "`n")
5757
It 'It fails when the only item not matching the filter is 0' {
5858
{ 0 | Should-All -FilterScript { $_ -gt 0 } } | Verify-AssertionFailed
5959
}
60+
61+
It 'Throws when provided unbound scriptblock' {
62+
# Unbound scriptblocks would execute in Pester's internal module state
63+
$ex = { 1 | Should-All ([scriptblock]::Create('')) } | Verify-Throw
64+
$ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
65+
}
6066
}

tst/functions/assert/Collection/Should-Any.Tests.ps1

+6
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,10 @@ Expected [int] 2, but got [int] 1." -replace "`r`n", "`n")
6161
It "Accepts FilterScript and Actual by position" {
6262
Should-Any { $true } 1, 2
6363
}
64+
65+
It 'Throws when provided unbound scriptblock' {
66+
# Unbound scriptblocks would execute in Pester's internal module state
67+
$ex = { 1 | Should-Any ([scriptblock]::Create('')) } | Verify-Throw
68+
$ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
69+
}
6470
}

tst/functions/assert/Exception/Should-Throw.Tests.ps1

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Describe "Should-Throw" {
1010
}
1111

1212
It "Passes when non-terminating exception is thrown" {
13-
1413
{ Write-Error "fail!" } | Should-Throw
1514
}
1615

@@ -23,6 +22,12 @@ Describe "Should-Throw" {
2322
Should-Throw 'MockErrorMessage' 'MockErrorId' ([Microsoft.PowerShell.Commands.WriteErrorException]) 'MockBecauseString'
2423
}
2524

25+
It 'Throws when provided unbound scriptblock' {
26+
# Unbound scriptblocks would execute in Pester's internal module state
27+
$ex = { ([scriptblock]::Create('')) | Should-Throw } | Verify-Throw
28+
$ex.Exception.Message | Verify-Like 'Unbound scriptblock*'
29+
}
30+
2631
Context "Filtering with exception type" {
2732
It "Passes when exception has the expected type" {
2833
{ throw [ArgumentException]"A is null!" } | Should-Throw -ExceptionType ([ArgumentException])

0 commit comments

Comments
 (0)