diff --git a/CHANGELOG.md b/CHANGELOG.md index 142a49758..7bb87e2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +## 8.8.0.0 + +- Ports fix for the following issue: + [Issue #142](https://github.com/PowerShell/PSDscResources/issues/142) + Fixes issue where MsiPackage Integration tests fail if the test HttpListener + fails to start. Moves the test HttpListener objects to dynamically assigned, + higher numbered ports to avoid conflicts with other services, and also checks + to ensure that the ports are available before using them. Adds checks to + ensure that no outstanding HTTP server jobs are running before attempting to + setup a new one. Also adds additional instrumentation to make it easier to + troubleshoot issues with the test HttpListener objects in the future. + ## 8.7.0.0 - MSFT_xWindowsProcess: diff --git a/DSCResources/MSFT_xMsiPackage/MSFT_xMsiPackage.psm1 b/DSCResources/MSFT_xMsiPackage/MSFT_xMsiPackage.psm1 index 5aec258a5..d47a72e6f 100644 --- a/DSCResources/MSFT_xMsiPackage/MSFT_xMsiPackage.psm1 +++ b/DSCResources/MSFT_xMsiPackage/MSFT_xMsiPackage.psm1 @@ -275,7 +275,7 @@ function Set-TargetResource } finally { - if ($null -ne $responseStream) + if ((Test-Path -Path variable:responseStream) -and ($null -ne $responseStream)) { Close-Stream -Stream $responseStream } diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 2fde639bc..8ffcd787c 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -1376,6 +1376,86 @@ function Set-UserPasswordUsingDirectoryEntry $null = $UserDE.SetInfo() } +<# + .SYNOPSIS + Finds an unused TCP port in the specified port range. By default, + searches within ports 38473 - 38799, which at the time of writing, show + as unassigned in: + https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + + .PARAMETER LowestPortNumber + The TCP port number at which to begin the unused port search. Must be + greater than 0. + + .PARAMETER HighestPortNumber + The highest TCP port number to search for unused ports within. Must be + greater than 0, and greater than LowestPortNumber. + + .PARAMETER ExcludePorts + TCP ports to exclude from the search, even if they fall within the + LowestPortNumber and HighestPortNumber range. +#> +function Get-UnusedTcpPort +{ + [OutputType([System.UInt16])] + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateScript({$_ -gt 0})] + [System.UInt16] + $LowestPortNumber = 38473, + + [Parameter()] + [ValidateScript({$_ -gt $0})] + [System.UInt16] + $HighestPortNumber = 38799, + + [Parameter()] + [System.UInt16[]] + $ExcludePorts = @() + ) + + if ($HighestPortNumber -lt $LowestPortNumber) + { + throw 'HighestPortNumber must be greater than or equal to LowestPortNumber' + } + + [System.UInt16] $unusedPort = 0 + + [System.Collections.ArrayList] $usedAndExcludedPorts = (Get-NetTCPConnection).LocalPort | Where-Object -FilterScript { + $_ -ge $LowestPortNumber -and $_ -le $HighestPortNumber + } + + if (!(Test-Path -Path variable:usedAndExcludedPorts) -or ($null -eq $usedAndExcludedPorts)) + { + [System.Collections.ArrayList] $usedAndExcludedPorts = @() + } + + if (!(Test-Path -Path variable:ExcludePorts) -or ($null -eq $ExcludePorts)) + { + $ExcludePorts = @() + } + + $null = $usedAndExcludedPorts.Add($ExcludePorts) + + foreach ($port in $LowestPortNumber..$HighestPortNumber) + { + if (!($usedAndExcludedPorts.Contains($port))) + { + $unusedPort = $port + break + } + } + + if ($unusedPort -eq 0) + { + throw "Failed to find unused TCP port between ports $LowestPortNumber and $HighestPortNumber." + } + + return $unusedPort +} + Export-ModuleMember -Function @( 'Add-PathPermission', 'Enter-DscResourceTestEnvironment', @@ -1391,5 +1471,6 @@ Export-ModuleMember -Function @( 'Test-IsFileLocked', 'Test-SetTargetResourceWithWhatIf', 'Test-SkipContinuousIntegrationTask', - 'Wait-ScriptBlockReturnTrue' + 'Wait-ScriptBlockReturnTrue', ` + 'Get-UnusedTcpPort' ) diff --git a/Tests/Integration/MSFT_xMsiPackage.EndToEnd.Tests.ps1 b/Tests/Integration/MSFT_xMsiPackage.EndToEnd.Tests.ps1 index 221ef3cc8..1e02d341b 100644 --- a/Tests/Integration/MSFT_xMsiPackage.EndToEnd.Tests.ps1 +++ b/Tests/Integration/MSFT_xMsiPackage.EndToEnd.Tests.ps1 @@ -53,6 +53,9 @@ Describe 'xMsiPackage End to End Tests' { $null = New-TestMsi -DestinationPath $script:msiLocation + $script:testHttpPort = Get-UnusedTcpPort + $script:testHttpsPort = Get-UnusedTcpPort -ExcludePorts @($script:testHttpPort) + # Clear the log file 'Beginning integration tests' > $script:logFile } @@ -370,8 +373,9 @@ Describe 'xMsiPackage End to End Tests' { Context 'Install package from HTTP Url' { $configurationName = 'UninstallExistingMsiPackageFromHttp' - $baseUrl = 'http://localhost:1242/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder = [System.UriBuilder]::new('http', 'localhost', $script:testHttpPort) + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -403,7 +407,10 @@ Describe 'xMsiPackage End to End Tests' { try { - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job @@ -438,8 +445,9 @@ Describe 'xMsiPackage End to End Tests' { Context 'Uninstall Msi package from HTTP Url' { $configurationName = 'InstallMsiPackageFromHttp' - $baseUrl = 'http://localhost:1242/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder = [System.UriBuilder]::new('http', 'localhost', $script:testHttpPort) + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -471,7 +479,10 @@ Describe 'xMsiPackage End to End Tests' { try { - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job @@ -506,8 +517,9 @@ Describe 'xMsiPackage End to End Tests' { Context 'Install Msi package from HTTPS Url' { $configurationName = 'InstallMsiPackageFromHttpS' - $baseUrl = 'https://localhost:1243/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder = [System.UriBuilder]::new('https', 'localhost', $script:testHttpsPort) + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -539,7 +551,10 @@ Describe 'xMsiPackage End to End Tests' { try { - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job @@ -574,8 +589,9 @@ Describe 'xMsiPackage End to End Tests' { Context 'Uninstall Msi package from HTTPS Url' { $configurationName = 'UninstallMsiPackageFromHttps' - $baseUrl = 'https://localhost:1243/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder = [System.UriBuilder]::new('https', 'localhost', $script:testHttpsPort) + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -607,7 +623,10 @@ Describe 'xMsiPackage End to End Tests' { try { - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job diff --git a/Tests/Integration/MSFT_xMsiPackage.Integration.Tests.ps1 b/Tests/Integration/MSFT_xMsiPackage.Integration.Tests.ps1 index d8e325a55..92bbe5539 100644 --- a/Tests/Integration/MSFT_xMsiPackage.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xMsiPackage.Integration.Tests.ps1 @@ -45,6 +45,9 @@ try $null = New-TestMsi -DestinationPath $script:msiLocation + $script:testHttpPort = Get-UnusedTcpPort + $script:testHttpsPort = Get-UnusedTcpPort -ExcludePorts @($script:testHttpPort) + # Clear the log file 'Beginning integration tests' > $script:logFile } @@ -167,8 +170,11 @@ try } It 'Should correctly install and remove a package from a HTTP URL' { - $baseUrl = 'http://localhost:1242/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder = [System.UriBuilder]::new('http', 'localhost', $script:testHttpPort) + $baseUrl = $uriBuilder.Uri.AbsoluteUri + + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -177,7 +183,10 @@ try { 'Http tests:' >> $script:logFile - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job @@ -192,6 +201,12 @@ try Set-TargetResource -Ensure 'Absent' -Path $msiUrl -ProductId $script:packageId Test-PackageInstalledById -ProductId $script:packageId | Should -Be $false } + catch + { + Write-Warning -Message 'Caught exception performing HTTP server tests. Outputting HTTP server log.' -Verbose + Get-Content -Path $script:logFile | Write-Verbose -Verbose + throw $_ + } finally { <# @@ -203,9 +218,11 @@ try } It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { + $uriBuilder = [System.UriBuilder]::new('https', 'localhost', $script:testHttpsPort) + $baseUrl = $uriBuilder.Uri.AbsoluteUri - $baseUrl = 'https://localhost:1243/' - $msiUrl = "$baseUrl" + 'package.msi' + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri $fileServerStarted = $null $job = $null @@ -214,7 +231,10 @@ try { 'Https tests:' >> $script:logFile - $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort $fileServerStarted = $serverResult.FileServerStarted $job = $serverResult.Job @@ -229,6 +249,12 @@ try Set-TargetResource -Ensure 'Absent' -Path $msiUrl -ProductId $script:packageId Test-PackageInstalledById -ProductId $script:packageId | Should -Be $false } + catch + { + Write-Warning -Message 'Caught exception performing HTTPS server tests. Outputting HTTPS server log.' -Verbose + Get-Content -Path $script:logFile | Write-Verbose -Verbose + throw $_ + } finally { <# diff --git a/Tests/MSFT_xPackageResource.TestHelper.psm1 b/Tests/MSFT_xPackageResource.TestHelper.psm1 index 95e58e1f5..a04b99aa1 100644 --- a/Tests/MSFT_xPackageResource.TestHelper.psm1 +++ b/Tests/MSFT_xPackageResource.TestHelper.psm1 @@ -8,6 +8,8 @@ param () $errorActionPreference = 'Stop' Set-StrictMode -Version 'Latest' +$testJobPrefix = 'MsiPackageTestJob' + <# .SYNOPSIS Tests if the package with the given Id is installed. @@ -62,9 +64,15 @@ function Test-PackageInstalledById .PARAMETER Https Indicates whether the server should use Https. If True then the file server will use Https - and listen on port 'https://localhost:1243'. Otherwise the file server will use Http and - listen on port 'http://localhost:1242' + and listen on port 'https://localhost:HttpsPort'. Otherwise the file server will use Http and + listen on port 'http://localhost:HttpPort' Default value is False (Http). + + .PARAMETER HttpPort + Specifies the TCP port to register an Http based HttpListener on. + + .PARAMETER HttspPort + Specifies the TCP port to register an Https based HttpListener on. #> function Start-Server { @@ -81,7 +89,17 @@ function Start-Server $LogPath = (Join-Path -Path $PSScriptRoot -ChildPath 'PackageTestLogFile.txt'), [System.Boolean] - $Https = $false + $Https = $false, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -gt 0})] + [System.UInt16] + $HttpPort, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -gt 0})] + [System.UInt16] + $HttpsPort ) # Create an event object to let the client know when the server is ready to begin receiving requests. @@ -97,7 +115,7 @@ function Start-Server #> $server = { - param($FilePath, $LogPath, $Https) + param($FilePath, $LogPath, $Https, $HttpPort, $HttpsPort) <# .SYNOPSIS @@ -108,6 +126,9 @@ function Start-Server .PARAMETER Https Indicates whether https was used and if so, removes the SSL binding. + + .PARAMETER HttspPort + Specifies the TCP port to de-register an Https based HttpListener from. #> function Stop-Listener { @@ -120,12 +141,17 @@ function Start-Server [Parameter(Mandatory = $true)] [System.Boolean] - $Https + $Https, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -gt 0})] + [System.UInt16] + $HttpsPort ) Write-Log -LogFile $LogPath -Message 'Finished listening for requests. Shutting down HTTP server.' - $ipPort = '0.0.0.0:1243' + $ipPort = "0.0.0.0:$HttpsPort" if ($null -eq $HttpListener) { @@ -166,11 +192,20 @@ function Start-Server <# .SYNOPSIS Creates and registers an SSL certificate for Https connections. + + .PARAMETER HttspPort + Specifies the TCP port to register an Https based HttpListener on. #> function Register-Ssl { [CmdletBinding()] - param() + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -gt 0})] + [System.UInt16] + $HttpsPort + ) # Create certificate $certificate = New-SelfSignedCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -DnsName localhost @@ -187,7 +222,7 @@ function Start-Server Write-Log -LogFile $LogPath -Message 'Finished importing certificate into root. About to bind it to port.' # Use net shell command to directly bind certificate to designated testing port - $null = netsh http add sslcert ipport=0.0.0.0:1243 certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable + $null = netsh http add sslcert ipport=0.0.0.0:$HttpsPort certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable } <# @@ -339,11 +374,11 @@ function Start-Server # Set up the listener if ($Https) { - $HttpListener.Prefixes.Add([System.Uri] 'https://localhost:1243') + $HttpListener.Prefixes.Add([Uri] "https://localhost:$HttpsPort") try { - Register-SSL + Register-SSL -HttpsPort $HttpsPort } catch { @@ -356,7 +391,7 @@ function Start-Server } else { - $HttpListener.Prefixes.Add([System.Uri] 'http://localhost:1242') + $HttpListener.Prefixes.Add([Uri] "http://localhost:$HttpPort") } Write-Log -LogFile $LogPath -Message 'Finished listener setup - about to start listener' @@ -457,8 +492,22 @@ function Start-Server catch { $errorMessage = "There were problems setting up the HTTP(s) listener. Error: $_" + Write-Log -LogFile $LogPath -Message $errorMessage - throw $errorMessage + + 'Error Record Info' >> $LogPath + $_ | ConvertTo-Xml -As String >> $LogPath + + 'Exception Info' >> $LogPath + $_.Exception | ConvertTo-Xml -As String >> $LogPath + + 'Running Process Info' >> $LogPath + Get-Process | Format-List | Out-String >> $LogPath + + 'Open TCP Connections Info' >> $LogPath + Get-NetTCPConnection | Format-List | Out-String >> $LogPath + + throw $_ } finally { @@ -468,11 +517,31 @@ function Start-Server } Write-Log -LogFile $LogPath -Message 'Stopping the Server' - Stop-Listener -HttpListener $HttpListener -Https $Https + Stop-Listener -HttpListener $HttpListener -Https $Https -HttpsPort $HttpsPort } } - $job = Start-Job -ScriptBlock $server -ArgumentList @( $FilePath, $LogPath, $Https ) + if ($Https) + { + $jobName = $testJobPrefix + 'Https' + } + else + { + $jobName = $testJobPrefix + 'Http' + } + + $job = Start-Job -ScriptBlock $server -Name $jobName -ArgumentList @( $FilePath, $LogPath, $Https, $HttpPort, $HttpsPort ) + + # Verify that the job is receivable and does not contain an exception. If it does, re-throw it. + try + { + $null = $job | Receive-Job + } + catch + { + Write-Error -Message 'Failed to setup HTTP(S) listener for MsiPackage Tests' + throw $_ + } <# Return the event object so that client knows when it can start sending requests and @@ -523,6 +592,20 @@ function Stop-Server } } +<# + .SYNOPSIS + Removes any jobs associated with HTTP(S) servers that were created + for MsiPackage tests. +#> +function Stop-EveryTestServerInstance +{ + [CmdletBinding()] + param () + + Get-Job -Name "$($testJobPrefix)*" | Stop-Job + Get-Job -Name "$($testJobPrefix)*" | Remove-Job +} + <# .SYNOPSIS Creates a new MSI package for testing. @@ -1147,115 +1230,6 @@ function Get-LocalizedRegistryKeyValue return $localizedRegistryKeyValue } -<# - .SYNOPSIS - Mimics a simple http or https file server. - Used only by the xPackage resource - xMsiPackage uses Start-Server instead - - .PARAMETER FilePath - The path to the file to add on the mock file server. - - .PARAMETER Https - Indicates that the new file server should use https. - Otherwise the new file server will use http. - Https functionality is not currently implemented in - this function - Start-Server should be used instead. -#> -function New-MockFileServer -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $FilePath, - - [System.Management.Automation.SwitchParameter] - $Https - ) - - if ($null -eq (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) - { - $null = New-NetFirewallRule -DisplayName 'UnitTestRule' -Direction 'Inbound' -Program "$PSHome\powershell.exe" -Authentication 'NotRequired' -Action 'Allow' - } - - netsh advfirewall set allprofiles state off - - Start-Job -ArgumentList @( $FilePath ) -ScriptBlock { - - # Create certificate - $certificate = Get-ChildItem -Path 'Cert:\LocalMachine\My' -Recurse | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -eq 'Server Authentication' } - - if ($certificate.Count -gt 1) - { - # Just use the first one - $certificate = $certificate[0] - } - elseif ($certificate.count -eq 0) - { - # Create a self-signed one - $certificate = New-SelfSignedCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -DnsName $env:computerName - } - - $hash = $certificate.Thumbprint - - # Use net shell command to directly bind certificate to designated testing port - netsh http add sslcert ipport=0.0.0.0:1243 certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable - - # Start listening endpoints - $httpListener = New-Object -TypeName 'System.Net.HttpListener' - - if ($Https) - { - $httpListener.Prefixes.Add([System.Uri] 'https://localhost:1243') - } - else - { - $httpListener.Prefixes.Add([System.Uri] 'http://localhost:1242') - } - - $httpListener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Negotiate - $httpListener.Start() - - # Create a pipe to flag http/https client - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.Connect() - $pipe.Dispose() - - # Prepare binary buffer for http/https response - $fileInfo = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList @( $args[0] ) - $numBytes = $fileInfo.Length - $fileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $args[0], 'Open' ) - $binaryReader = New-Object -TypeName 'System.IO.BinaryReader' -ArgumentList @( $fileStream ) - [System.Byte[]] $buf = $binaryReader.ReadBytes($numBytes) - $fileStream.Close() - - # Send response - $response = ($httpListener.GetContext()).Response - $response.ContentType = 'application/octet-stream' - $response.ContentLength64 = $buf.Length - $response.OutputStream.Write($buf, 0, $buf.Length) - $response.OutputStream.Flush() - - # Wait for client to finish downloading - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.WaitForConnection() - $pipe.Dispose() - - $response.Dispose() - $httpListener.Stop() - $httpListener.Close() - - # Close pipe - - # Use net shell command to clean up the certificate binding - netsh http delete sslcert ipport=0.0.0.0:1243 - } - - netsh advfirewall set allprofiles state on -} - <# .SYNOPSIS Creates a new test executable. @@ -1320,8 +1294,8 @@ Export-ModuleMember -Function ` New-TestMsi, ` Clear-PackageCache, ` New-TestExecutable, ` - New-MockFileServer, ` Start-Server, ` Stop-Server, ` Test-PackageInstalledByName, ` - Test-PackageInstalledById + Test-PackageInstalledById, ` + Stop-EveryTestServerInstance diff --git a/Tests/Unit/MSFT_xMsiPackage.Tests.ps1 b/Tests/Unit/MSFT_xMsiPackage.Tests.ps1 index 2f2deae4b..d969f2b4f 100644 --- a/Tests/Unit/MSFT_xMsiPackage.Tests.ps1 +++ b/Tests/Unit/MSFT_xMsiPackage.Tests.ps1 @@ -236,13 +236,13 @@ Describe 'xMsiPackage Unit Tests' { @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } - @{ Command = 'Test-Path'; Times = 2; Custom = 'to the package cache' } + @{ Command = 'Test-Path'; Times = 3; Custom = 'to the package cache' } @{ Command = 'New-Item'; Times = 0; Custom = 'directory for the package cache' } @{ Command = 'New-Object'; Times = 1; Custom = 'file stream to copy the response to' } @{ Command = 'Get-WebRequestResponse'; Times = 1 } @{ Command = 'Copy-ResponseStreamToFileStream'; Times = 1 } @{ Command = 'Close-Stream'; Times = 2 } - @{ Command = 'Test-Path'; Times = 2; Custom = 'to the MSI file' } + @{ Command = 'Test-Path'; Times = 3; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @@ -267,13 +267,13 @@ Describe 'xMsiPackage Unit Tests' { @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } - @{ Command = 'Test-Path'; Times = 2; Custom = 'to the package cache' } + @{ Command = 'Test-Path'; Times = 3; Custom = 'to the package cache' } @{ Command = 'New-Item'; Times = 0; Custom = 'directory for the package cache' } @{ Command = 'New-Object'; Times = 1; Custom = 'file stream to copy the response to' } @{ Command = 'Get-WebRequestResponse'; Times = 1 } @{ Command = 'Copy-ResponseStreamToFileStream'; Times = 1 } @{ Command = 'Close-Stream'; Times = 2 } - @{ Command = 'Test-Path'; Times = 2; Custom = 'to the MSI file' } + @{ Command = 'Test-Path'; Times = 3; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @@ -530,23 +530,25 @@ Describe 'xMsiPackage Unit Tests' { } It 'Should return the expected URI when scheme is http' { - $filePath = 'http://localhost:1242/testMsi.msi' - $expectedReturnValue = [System.Uri] $filePath + $uriBuilder = [System.UriBuilder]::new('http', 'localhost') + $uriBuilder.Path = 'testMsi.msi' + $filePath = $uriBuilder.Uri.AbsoluteUri - Convert-PathToUri -Path $filePath | Should -Be $expectedReturnValue + Convert-PathToUri -Path $filePath | Should -Be $uriBuilder.Uri } It 'Should return the expected URI when scheme is https' { - $filePath = 'https://localhost:1243/testMsi.msi' - $expectedReturnValue = [System.Uri] $filePath + $uriBuilder = [System.UriBuilder]::new('https', 'localhost') + $uriBuilder.Path = 'testMsi.msi' + $filePath = $uriBuilder.Uri.AbsoluteUri - Convert-PathToUri -Path $filePath | Should -Be $expectedReturnValue + Convert-PathToUri -Path $filePath | Should -Be $uriBuilder.Uri } } Context 'Invalid path passed in' { It 'Should throw an error when uri scheme is invalid' { - $filePath = 'ht://localhost:1243/testMsi.msi' + $filePath = 'ht://localhost/testMsi.msi' $expectedErrorMessage = ($script:localizedData.InvalidPath -f $filePath) { Convert-PathToUri -Path $filePath } | Should -Throw -ExpectedMessage $expectedErrorMessage diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 60a3b08f5..cc80a04ab 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -39,6 +39,12 @@ try $null = New-Item -Path $script:testDirectoryPath -ItemType 'Directory' + <# + This log file is used to log messages from the mock server which is important for debugging since + most of the work of the mock server is done within a separate process. + #> + $script:logFile = Join-Path -Path $PSScriptRoot -ChildPath 'PackageTestLogFile.txt' + $script:msiName = 'DSCSetupProject.msi' $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName $script:msiArguments = '/NoReboot' @@ -48,6 +54,9 @@ try $null = New-TestMsi -DestinationPath $script:msiLocation + $script:testHttpPort = Get-UnusedTcpPort + $script:testHttpsPort = Get-UnusedTcpPort -ExcludePorts @($script:testHttpPort) + $script:testExecutablePath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestExecutable.exe' $null = New-TestExecutable -DestinationPath $script:testExecutablePath @@ -423,49 +432,99 @@ try } It 'Should correctly install and remove a package from a HTTP URL' { - $baseUrl = 'http://localhost:1242/' - $msiUrl = "$baseUrl" + 'package.msi' - New-MockFileServer -FilePath $script:msiLocation + $uriBuilder = [System.UriBuilder]::new('http', 'localhost', $script:testHttpPort) + $baseUrl = $uriBuilder.Uri.AbsoluteUri - # Test pipe connection as testing server readiness - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.WaitForConnection() - $pipe.Dispose() + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri - { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should -Throw + $fileServerStarted = $null + $job = $null - Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalledByName -Name $script:packageName | Should -Be $true + try + { + 'Http tests:' >> $script:logFile - Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalledByName -Name $script:packageName | Should -Be $false + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance + + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $false -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort + $fileServerStarted = $serverResult.FileServerStarted + $job = $serverResult.Job + + # Wait for the file server to be ready to receive requests + $fileServerStarted.WaitOne(30000) + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should -Throw - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.Connect() - $pipe.Dispose() + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should -Be $true + + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should -Be $false + } + catch + { + Write-Warning -Message 'Caught exception performing HTTP server tests. Outputting HTTP server log.' -Verbose + Get-Content -Path $script:logFile | Write-Verbose -Verbose + throw $_ + } + finally + { + <# + This must be called after Start-Server to ensure the listening port is closed, + otherwise subsequent tests may fail until the machine is rebooted. + #> + Stop-Server -FileServerStarted $fileServerStarted -Job $job + } } It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { - $baseUrl = 'https://localhost:1243/' - $msiUrl = "$baseUrl" + 'package.msi' - New-MockFileServer -FilePath $script:msiLocation -Https + $uriBuilder = [System.UriBuilder]::new('https', 'localhost', $script:testHttpsPort) + $baseUrl = $uriBuilder.Uri.AbsoluteUri - # Test pipe connection as testing server reasdiness - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.WaitForConnection() - $pipe.Dispose() + $uriBuilder.Path = 'package.msi' + $msiUrl = $uriBuilder.Uri.AbsoluteUri - { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should -Throw + $fileServerStarted = $null + $job = $null - Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalledByName -Name $script:packageName | Should -Be $true + try + { + 'Https tests:' >> $script:logFile - Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalledByName -Name $script:packageName | Should -Be $false + # Make sure no existing HTTP(S) test servers are running + Stop-EveryTestServerInstance - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.Connect() - $pipe.Dispose() + $serverResult = Start-Server -FilePath $script:msiLocation -LogPath $script:logFile -Https $true -HttpPort $script:testHttpPort -HttpsPort $script:testHttpsPort + $fileServerStarted = $serverResult.FileServerStarted + $job = $serverResult.Job + + # Wait for the file server to be ready to receive requests + $fileServerStarted.WaitOne(30000) + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should -Throw + + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should -Be $true + + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should -Be $false + } + catch + { + Write-Warning -Message 'Caught exception performing HTTPS server tests. Outputting HTTPS server log.' -Verbose + Get-Content -Path $script:logFile | Write-Verbose -Verbose + throw $_ + } + finally + { + <# + This must be called after Start-Server to ensure the listening port is closed, + otherwise subsequent tests may fail until the machine is rebooted. + #> + Stop-Server -FileServerStarted $fileServerStarted -Job $job + } } It 'Should write to the specified log path' { diff --git a/xPSDesiredStateConfiguration.psd1 b/xPSDesiredStateConfiguration.psd1 index d87afc8aa..7f2c2d714 100644 --- a/xPSDesiredStateConfiguration.psd1 +++ b/xPSDesiredStateConfiguration.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. - moduleVersion = '8.7.0.0' + moduleVersion = '8.8.0.0' # ID used to uniquely identify this module GUID = 'cc8dc021-fa5f-4f96-8ecf-dfd68a6d9d48' @@ -52,27 +52,15 @@ All of the resources in the DSC Resource Kit are provided AS IS, and are not sup # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '- MSFT_xWindowsProcess: - - Fixes issue where a process will fail to be created if a $Path is passed - that contains one or more spaces, and the resource is using $Credentials. - - Fixes issue where a process will fail to be created if $Arguments are - passed that contain one or more spaces (with or without credentials). - - Fixes issue where Integration tests fail if empty Arguments are passed. - [issue 605](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/605) - - Heavily refactors MSFT_xWindowsProcess.Integration.Tests.ps1 and adds more - Path and Arguments related test cases. - - Removes reliance on test file WindowsProcessTestProcess. -- Fixes test failures in xWindowsOptionalFeatureSet.Integration.Tests.ps1 due - to accessing the windowsOptionalFeatureName variable before it is assigned. - [issue 612](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/612) -- MSFT_xDSCWebService - - Fixes [issue - 536](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/536) - and starts the deprecation process for configuring a windows firewall - (exception) rule using xDSCWebService - - Fixes [issue - 463](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/463) - and fixes some bugs introduced with the new firewall rule handling + ReleaseNotes = '- Ports fix for the following issue: + [Issue 142](https://github.com/PowerShell/PSDscResources/issues/142) + Fixes issue where MsiPackage Integration tests fail if the test HttpListener + fails to start. Moves the test HttpListener objects to dynamically assigned, + higher numbered ports to avoid conflicts with other services, and also checks + to ensure that the ports are available before using them. Adds checks to + ensure that no outstanding HTTP server jobs are running before attempting to + setup a new one. Also adds additional instrumentation to make it easier to + troubleshoot issues with the test HttpListener objects in the future. ' @@ -82,3 +70,4 @@ All of the resources in the DSC Resource Kit are provided AS IS, and are not sup +