PowerShell/test/powershell/Language/Parser/ParameterBinding.Tests.ps1
2021-08-30 13:56:52 -07:00

475 lines
19 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'Argument transformation attribute on optional argument with explicit $null' -Tags "CI" {
$tdefinition = @'
using System;
using System.Management.Automation;
using System.Reflection;
namespace MSFT_1407291
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class AddressTransformationAttribute : ArgumentTransformationAttribute
{
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
return (ulong) 42;
}
}
[Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesUInt64")]
[OutputType(typeof(System.String))]
public class Cmdlet1 : PSCmdlet
{
[Parameter(Mandatory = false)]
[AddressTransformation]
public ulong Address { get; set; }
protected override void ProcessRecord()
{
WriteObject(Address);
}
}
[Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesObject")]
[OutputType(typeof(System.String))]
public class Cmdlet2 : PSCmdlet
{
[Parameter(Mandatory = false)]
[AddressTransformation]
public object Address { get; set; }
protected override void ProcessRecord()
{
WriteObject(Address ?? "passed in null");
}
}
}
'@
$mod = Add-Type -PassThru -TypeDefinition $tdefinition
Import-Module $mod[0].Assembly -ErrorVariable ErrorImportingModule
function Invoke-ScriptFunctionTakesObject
{
param([MSFT_1407291.AddressTransformation()]
[Parameter(Mandatory = $false)]
[object]$Address = "passed in null")
return $Address
}
function Invoke-ScriptFunctionTakesUInt64
{
param([MSFT_1407291.AddressTransformation()]
[Parameter(Mandatory = $false)]
[Uint64]$Address = 11)
return $Address
}
It "There was no error importing the in-memory module" {
$ErrorImportingModule | Should -BeNullOrEmpty
}
It "Script function takes object" {
Invoke-ScriptFunctionTakesObject | Should -Be 42
}
It "Script function takes uint64" {
Invoke-ScriptFunctionTakesUInt64 | Should -Be 42
}
It "csharp cmdlet takes object" {
Invoke-CSharpCmdletTakesObject | Should -Be "passed in null"
}
It "csharp cmdlet takes uint64" {
Invoke-CSharpCmdletTakesUInt64 | Should -Be 0
}
It "script function takes object when parameter is null" {
Invoke-ScriptFunctionTakesObject -Address $null | Should -Be 42
}
It "script function takes unit64 when parameter is null" {
Invoke-ScriptFunctionTakesUInt64 -Address $null | Should -Be 42
}
It "script csharp cmdlet takes object when parameter is null" {
Invoke-CSharpCmdletTakesObject -Address $null | Should -Be 42
}
It "script csharp cmdlet takes uint64 when parameter is null" {
Invoke-CSharpCmdletTakesUInt64 -Address $null | Should -Be 42
}
}
Describe "Custom type conversion in parameter binding" -Tags 'Feature' {
BeforeAll {
## Prepare the script module
$content = @'
function Test-ScriptCmdlet {
[CmdletBinding(DefaultParameterSetName = "File")]
param(
[Parameter(Mandatory, ParameterSetName = "File")]
[System.IO.FileInfo] $File,
[Parameter(Mandatory, ParameterSetName = "StartInfo")]
[System.Diagnostics.ProcessStartInfo] $StartInfo
)
if ($PSCmdlet.ParameterSetName -eq "File") {
$File.Name
} else {
$StartInfo.FileName
}
}
function Test-ScriptFunction {
param(
[System.IO.FileInfo] $File,
[System.Diagnostics.ProcessStartInfo] $StartInfo
)
if ($null -ne $File) {
$File.Name
}
if ($null -ne $StartInfo) {
$StartInfo.FileName
}
}
'@
Set-Content -Path $TestDrive\module.psm1 -Value $content -Force
## Prepare the C# module
$code = @'
using System.IO;
using System.Diagnostics;
using System.Management.Automation;
namespace Test
{
[Cmdlet("Test", "BinaryCmdlet", DefaultParameterSetName = "File")]
public class TestCmdletCommand : PSCmdlet
{
[Parameter(Mandatory = true, ParameterSetName = "File")]
public FileInfo File { get; set; }
[Parameter(Mandatory = true, ParameterSetName = "StartInfo")]
public ProcessStartInfo StartInfo { get; set; }
protected override void ProcessRecord()
{
if (this.ParameterSetName == "File")
{
WriteObject(File.Name);
}
else
{
WriteObject(StartInfo.FileName);
}
}
}
}
'@
if ($IsWindows) {
$asmFile = [System.IO.Path]::GetTempFileName() + ".dll"
}
else {
$asmFile = (Join-Path $env:HOME $([System.IO.Path]::GetRandomFileName() + ".dll"))
}
Add-Type -TypeDefinition $code -OutputAssembly $asmFile
## Helper function to execute script
function Execute-Script {
[CmdletBinding(DefaultParameterSetName = "Script")]
param(
[Parameter(Mandatory)]
[powershell]$ps,
[Parameter(Mandatory, ParameterSetName = "Script")]
[string]$Script,
[Parameter(Mandatory, ParameterSetName = "Command")]
[string]$Command,
[Parameter(Mandatory, ParameterSetName = "Command")]
[string]$ParameterName,
[Parameter(Mandatory, ParameterSetName = "Command")]
[object]$Argument
)
$ps.Commands.Clear()
$ps.Streams.ClearStreams()
if ($PSCmdlet.ParameterSetName -eq "Script") {
$ps.AddScript($Script).Invoke()
} else {
$ps.AddCommand($Command).AddParameter($ParameterName, $Argument).Invoke()
}
}
## Helper command strings
$changeToConstrainedLanguage = '$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"'
$getLanguageMode = '$ExecutionContext.SessionState.LanguageMode'
$importScriptModule = "Import-Module $TestDrive\module.psm1"
$importCSharpModule = "Import-Module $asmFile"
}
AfterAll {
## Set the LanguageMode to force rebuilding the type conversion cache.
## This is needed because type conversions happen in the new powershell runspace with 'ConstrainedLanguage' mode
## will be put in the type conversion cache, and that may affect the default session.
$ExecutionContext.SessionState.LanguageMode = "FullLanguage"
}
It "Custom type conversion in parameter binding is allowed in FullLanguage" {
## Create a powershell instance for the test
$ps = [powershell]::Create()
try {
## Import the modules in FullLanguage mode
Execute-Script -ps $ps -Script $importScriptModule
Execute-Script -ps $ps -Script $importCSharpModule
$languageMode = Execute-Script -ps $ps -Script $getLanguageMode
$languageMode | Should -Be 'FullLanguage'
$result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse"
$result1 | Should -Be "fileToUse"
$result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse"
$result2 | Should -Be "fileToUse"
$result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse"
$result3 | Should -Be "fileToUse"
## Conversion involves setting properties of an instance of the target type is allowed in FullLanguage mode
$hashValue = @{ FileName = "filename"; Arguments = "args" }
$psobjValue = [PSCustomObject] $hashValue
## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties
$result4 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue
$result4 | Should -Be "filename"
$result5 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
$result5 | Should -Be "filename"
## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties
$result6 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue
$result6 | Should -Be "filename"
$result7 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue
$result7 | Should -Be "filename"
## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties
$result8 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue
$result8 | Should -Be "filename"
$result9 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
$result9 | Should -Be "filename"
}
finally {
$ps.Dispose()
}
}
It "Some custom type conversion in parameter binding is allowed for trusted cmdlets in ConstrainedLanguage" {
## Create a powershell instance for the test
$ps = [powershell]::Create()
try {
## Import the modules in FullLanguage mode
Execute-Script -ps $ps -Script $importScriptModule
Execute-Script -ps $ps -Script $importCSharpModule
$languageMode = Execute-Script -ps $ps -Script $getLanguageMode
$languageMode | Should -Be 'FullLanguage'
## Change to ConstrainedLanguage mode
Execute-Script -ps $ps -Script $changeToConstrainedLanguage
$languageMode = Execute-Script -ps $ps -Script $getLanguageMode
$languageMode | Should -Be 'ConstrainedLanguage'
$result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse"
$result1 | Should -Be "fileToUse"
$result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse"
$result2 | Should -Be "fileToUse"
$result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse"
$result3 | Should -Be "fileToUse"
## If the conversion involves setting properties of an instance of the target type,
## then it's disallowed even for trusted cmdlets.
$hashValue = @{ FileName = "filename"; Arguments = "args" }
$psobjValue = [PSCustomObject] $hashValue
## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script"
}
}
finally {
$ps.Dispose()
}
}
It "Custom type conversion in parameter binding is NOT allowed for untrusted cmdlets in ConstrainedLanguage" {
## Create a powershell instance for the test
$ps = [powershell]::Create()
try {
$languageMode = Execute-Script -ps $ps -Script $getLanguageMode
$languageMode | Should -Be 'FullLanguage'
## Change to ConstrainedLanguage mode
Execute-Script -ps $ps -Script $changeToConstrainedLanguage
$languageMode = Execute-Script -ps $ps -Script $getLanguageMode
$languageMode | Should -Be 'ConstrainedLanguage'
## Import the modules in ConstrainedLanguage mode
Execute-Script -ps $ps -Script $importScriptModule
Execute-Script -ps $ps -Script $importCSharpModule
$result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse"
$result1 | Should -Be $null
$ps.Streams.Error.Count | Should -Be 1
$ps.Streams.Error[0].FullyQualifiedErrorId | Should -Be "ParameterArgumentTransformationError,Test-ScriptCmdlet"
$result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse"
$result2 | Should -Be $null
$ps.Streams.Error.Count | Should -Be 1
$ps.Streams.Error[0].FullyQualifiedErrorId | Should -Be "ParameterArgumentTransformationError,Test-ScriptFunction"
## Binary cmdlets are always marked as trusted because only trusted assemblies can be loaded on DeviceGuard machine.
$result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse"
$result3 | Should -Be "fileToUse"
## Conversion that involves setting properties of an instance of the target type is disallowed.
$hashValue = @{ FileName = "filename"; Arguments = "args" }
$psobjValue = [PSCustomObject] $hashValue
## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingArgumentTransformationException,Execute-Script"
}
## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties
try {
Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script"
}
try {
Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue
throw "Expected exception was not thrown!"
} catch {
$_.FullyQualifiedErrorId | Should -Be "ParameterBindingException,Execute-Script"
}
}
finally {
$ps.Dispose()
}
}
}
Describe 'Roundtrippable Conversions for Bare-string Numeric Literals passed to [string] Parameters' -Tags CI {
BeforeAll {
$TestValues = @(
@{ Argument = "34uy" }
@{ Argument = "48y" }
@{ Argument = "8s" }
@{ Argument = "49us" }
@{ Argument = "26" }
@{ Argument = "28u" }
@{ Argument = "24l" }
@{ Argument = "32ul" }
@{ Argument = "20d" }
@{ Argument = "6n" }
)
function Test-SimpleStringValue([string] $Value) { $Value }
function Test-AdvancedStringValue {
[CmdletBinding()]
param(
[string]
$Value
)
$Value
}
}
It 'should correctly convert <Argument> back to string in simple functions' -TestCases $TestValues {
param($Argument)
Invoke-Expression "Test-SimpleStringValue -Value $Argument" | Should -BeExactly $Argument
}
It 'should correctly convert <Argument> back to string in advanced functions' -TestCases $TestValues {
param($Argument)
Invoke-Expression "Test-AdvancedStringValue -Value $Argument" | Should -BeExactly $Argument
}
}