Description
General summary of the issue
When mocking Out-File
and invoking it in code using Out-File -FilePath $path -Encoding utf8NoBOM -Force;
, Pester produces this error when executing a test:
[-] Out-WebConfig.outputs file to expected output path when input path exists 14ms (14ms|1ms)
PSInvalidCastException: Cannot convert the "utf8NoBOM" value of type "System.String" to type "System.Text.Encoding".
ArgumentTransformationMetadataException: Cannot convert the "utf8NoBOM" value of type "System.String" to type "System.Text.Encoding".
ParameterBindingArgumentTransformationException: Cannot process argument transformation on parameter 'Encoding'. Cannot convert the "utf8NoBOM" value of type "System.String" to type "System.Text.Encoding".
at Out-WebConfig, D:\Git\Clients\myclient\AzureRepos\myclient.BuildTemplates\scripts\WebConfigHelpers\WebConfigHelpers.psm1:61
at <ScriptBlock>, D:\Git\Clients\myclient\AzureRepos\myclient.BuildTemplates\test\WebConfigHelpers.Tests.ps1:48
'utf8NoBom' is a new value available when using Out-File with PowerShell Core. They updated it to make Out-File (and various other cmdlets) actually useful to Windows users (previously BOMs would always be output when using UTF8, causing no end of encoding issues). It seems that the Encoding
parameter now accepts more than just an enum / string value, which is where I expect this issue is coming from - possible that Pester's mocking functions don't cater for this?
I have been trying to find the specification of this column in PowerShell Core (OSS github code and MS's documentation) but all I've found are these links (which aren't all that useful as I assume that Out-File cmdlet is doing some input param management before it calls any .Net functions):
https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding?view=net-5.0
https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding.codepage?view=netcore-2.2
Describe your environment
Pester version : 5.0.2 C:\Users\****\Documents\PowerShell\Modules\Pester\5.0.2\Pester.psm1
PowerShell version : 7.1.3
OS version : Microsoft Windows NT 10.0.18363.0
Steps to reproduce
All code for my specific implementation here (it's in a PS module):
#requires -Version 7;
Set-PSDebug -Strict;
$ErrorActionPreference = 'Stop';
function Format-XML ([xml]$xml, $indent = 2) {
$StringWriter = New-Object System.IO.StringWriter;
$XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter;
$xmlWriter.Formatting = "indented"
$xmlWriter.Indentation = $Indent;
$xml.WriteContentTo($XmlWriter);
$XmlWriter.Flush();
$StringWriter.Flush();
Write-Output $StringWriter.ToString();
}
function Out-WebConfig {
Param(
[Parameter(Mandatory = $True)]
[string]$inputWebConfigPath,
[Parameter(Mandatory = $True)]
[string]$outputWebConfigPath,
[Parameter(Mandatory = $True)]
[string]$environment
)
Set-PSDebug -Strict;
$ErrorActionPreference = 'Stop';
$isVerbose = ($PSBoundParameters['Verbose'] -eq $true);
# Read the XML content
if (!(Test-Path -Path $inputWebConfigPath)) {
# Generally not OK, but we don't want this task to fail in this instance.
return;
}
$xml = [xml](Get-Content -Path $inputWebConfigPath -Raw);
# Assume that system.webServer already exists
$sws = $xml.SelectSingleNode("//system.webServer");
# Create the environment elements
$evs = $xml.CreateElement("environmentVariables");
$ev = $xml.CreateElement("environmentVariable");
$ev.SetAttribute("name", "ASPNETCORE_ENVIRONMENT");
$ev.SetAttribute("value", $environment);
$evs.AppendChild($ev) | Out-Null;
# Add the new environment elements
$anc = $sws.SelectSingleNode("aspNetCore");
if(!$anc) {
$anc = $xml.CreateElement("aspNetCore");
$sws.AppendChild($anc) | Out-Null;
}
$anc.AppendChild($evs) | Out-Null;
# Output a formatted XML document
Format-XML $xml.OuterXml | Out-File -FilePath $outputWebConfigPath -Encoding utf8NoBOM -Force;
}
Export-ModuleMember -Function Out-WebConfig;
Pester test code here. First test passes (does not hit the code in question - mocks aren't required but included for completeness), second test fails regardless of the assertion params specified:
#requires -Version 7;
Set-PSDebug -Strict;
$solutionRoot = Resolve-Path "$PSScriptRoot\..";
Import-Module $solutionRoot\scripts\WebConfigHelpers -Force;
InModuleScope WebConfigHelpers {
BeforeAll {
$inputXmlWithAspNetCoreEntry = @'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<aspNetCore />
</system.webServer>
</configuration>
'@;
}
Describe "Out-WebConfig" {
It "does not attempt to read our output XML when input path does not exist" {
# Arrange
$expectedInputWebConfigPath = "C:\a\path.config";
$expectedOutputWebConfigPath = "C:\b\path.config";
$expectedEnvironment = "MYENV-QA1";
Mock Test-Path { return $False; };
Mock Get-Content { return $null; };
Mock Out-File { return $null; };
# Act
Out-WebConfig -inputWebConfigPath $expectedInputWebConfigPath -outputWebConfigPath $expectedOutputWebConfigPath -environment $expectedEnvironment;
# Assert
Should -Invoke Get-Content -Times 0;
Should -Invoke Out-File -Times 0;
}
It "outputs file to expected output path when input path exists" {
# Arrange
$expectedInputWebConfigPath = "C:\a\path.config";
$expectedOutputWebConfigPath = "C:\b\path.config";
$expectedEnvironment = "MYENV-QA1";
$inputXml = $inputXmlWithAspNetCoreEntry;
Mock Test-Path { return $True; };
Mock Get-Content { return $inputXml; };
Mock Out-File { return $null; };
# Act
Out-WebConfig -inputWebConfigPath $expectedInputWebConfigPath -outputWebConfigPath $expectedOutputWebConfigPath -environment $expectedEnvironment;
# Assert
Should -Invoke Out-File -Times 1 -ExclusiveFilter {
$FilePath -eq $expectedOutputWebConfigPath -and
$Encoding -eq 'utf8NoBOM'
};
}
}
}
Expected Behavior
I expect to be able to arbitrarily mock Out-File and "ignore" its underlying operation so I can test various cmdlet inputs without writing files to disk. At present the only way I can really run these tests are by outputting the files into the temp dir and cleaning them up afterwards.