/
githubmirror
/
PowerShell
Обзор
Документация
Войти
/
githubmirror
/
PowerShell
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1
4 753 строки
228 KB
yotsuda
Fix Invoke-RestMethod to support read-only files in multipart form data (#26454)
18 ноя 2025, 15:49
Не верифицирован
18 ноя 2025, 15:49
d486365
Код
Авторство
О чём код?
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # This is a Pester test suite which validate the Web cmdlets. # # Note: These tests use data from WebListener # # Invokes the given command via script block invocation. # function ExecuteWebCommand { param ( [ValidateNotNullOrEmpty()] [string] $command ) $result = [PSObject]@{Output = $null; Error = $null} try { $scriptBlock = [scriptblock]::Create($command) $result.Output = & $scriptBlock } catch { $result.Error = $_ } return $result } # This function calls either Invoke-WebRequest or Invoke-RestMethod using the OutFile parameter # Then, the file content is read and return in a $result object. # function ExecuteRequestWithOutFile { param ( [ValidateSet("Invoke-RestMethod", "Invoke-WebRequest" )] [string] $cmdletName, [string] $uri = (Get-WebListenerUrl -Test 'Get') ) $result = [PSObject]@{Output = $null; Error = $null} # We use '[outfile1]' in the file name to check that OutFile parameter is literal path $filePath = Join-Path $TestDrive ((Get-Random).ToString() + "[outfile1].txt") try { if ($cmdletName -eq "Invoke-WebRequest") { Invoke-WebRequest -Uri $uri -OutFile $filePath } else { Invoke-RestMethod -Uri $uri -OutFile $filePath } $result.Output = Get-Content -LiteralPath $filePath -Raw -ErrorAction SilentlyContinue } catch { $result.Error = $_ } finally { if (Test-Path $filePath) { Remove-Item $filePath -Force -ErrorAction SilentlyContinue } } return $result } # This function calls either Invoke-WebRequest or Invoke-RestMethod with the given uri # using the Headers parameter to disable keep-alive. # function ExecuteRequestWithHeaders { param ( [ValidateSet("Invoke-RestMethod", "Invoke-WebRequest" )] [string] $cmdletName, [string] $uri = (Get-WebListenerUrl -Test 'Get') ) $result = [PSObject]@{Output = $null; Error = $null} try { $headers = @{ Connection = 'close'} if ($cmdletName -eq "Invoke-WebRequest") { $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers } else { $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers } } catch { $result.Error = $_ } return $result } # Returns test data for the given content type. # function GetTestData { param ( [ValidateSet("text/plain", "application/xml", "application/json")] [String] $contentType ) $testData = @{ItemID = 987123; Name = 'TestData'} if ($contentType -eq "text/plain") { $body = $testData | Out-String } elseif ($contentType -eq "application/xml") { $body = ' <?xml version="1.0" encoding="utf-8"?> <Objects> <Object> <ItemID>987123</ItemID> <Name>TestData</Name> </Object> </Objects> ' } else { # "application/json" $body = $testData | ConvertTo-Json -Compress } return $body } function ExecuteRedirectRequest { param ( [Parameter(Mandatory)] [string] $uri, [ValidateSet('Invoke-WebRequest', 'Invoke-RestMethod')] [string] $Cmdlet = 'Invoke-WebRequest', [ValidateSet('POST', 'GET')] [string] $Method = 'GET', [ValidateSet('POST')] [string] $CustomMethod, [switch] $PreserveAuthorizationOnRedirect, [switch] $PreserveHttpMethodOnRedirect, [ValidateRange(0, [int]::MaxValue)] [int] $MaximumRedirection ) $result = [PSObject]@{Output = $null; Error = $null; Content = $null} try { $headers = @{"Authorization" = "test"} if ($Cmdlet -eq 'Invoke-WebRequest') { if ($MaximumRedirection -gt 0) { $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method -MaximumRedirection:$MaximumRedirection } elseif ($CustomMethod) { $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -CustomMethod $CustomMethod } else { $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -PreserveHttpMethodOnRedirect:$PreserveHttpMethodOnRedirect.IsPresent -Method $Method } $result.Content = $result.Output.Content | ConvertFrom-Json } else { if ($MaximumRedirection -gt 0) { $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method -MaximumRedirection:$MaximumRedirection } elseif ($CustomMethod) { $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -CustomMethod $CustomMethod } else { $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -PreserveHttpMethodOnRedirect:$PreserveHttpMethodOnRedirect.IsPresent -Method $Method } # NOTE: $result.Output should already be a PSObject (Invoke-RestMethod converts the returned json automatically) # so simply reference $result.Output $result.Content = $result.Output } } catch { $result.Error = $_ } return $result } # This function calls either Invoke-WebRequest or Invoke-RestMethod with the given uri # using the custum headers and the optional SkipHeaderValidation switch. function ExecuteRequestWithCustomHeaders { param ( [Parameter(Mandatory)] [string] $Uri, [ValidateSet('Invoke-WebRequest', 'Invoke-RestMethod')] [string] $Cmdlet = 'Invoke-WebRequest', [Parameter(Mandatory)] [ValidateNotNull()] [Hashtable] $Headers, [switch] $SkipHeaderValidation ) $result = [PSObject]@{Output = $null; Error = $null; Content = $null} try { if ($Cmdlet -eq 'Invoke-WebRequest') { $result.Output = Invoke-WebRequest -Uri $Uri -Headers $Headers -SkipHeaderValidation:$SkipHeaderValidation.IsPresent $result.Content = $result.Output.Content | ConvertFrom-Json } else { $result.Output = Invoke-RestMethod -Uri $Uri -Headers $Headers -SkipHeaderValidation:$SkipHeaderValidation.IsPresent # NOTE: $result.Output should already be a PSObject (Invoke-RestMethod converts the returned json automatically) # so simply reference $result.Output $result.Content = $result.Output } } catch { $result.Error = $_ } return $result } # This function calls either Invoke-WebRequest or Invoke-RestMethod with the given uri # using the custom UserAgent and the optional SkipHeaderValidation switch. function ExecuteRequestWithCustomUserAgent { param ( [Parameter(Mandatory)] [string] $Uri, [ValidateSet('Invoke-WebRequest', 'Invoke-RestMethod')] [string] $Cmdlet = 'Invoke-WebRequest', [Parameter(Mandatory)] [ValidateNotNull()] [string] $UserAgent, [switch] $SkipHeaderValidation ) $result = [PSObject]@{Output = $null; Error = $null; Content = $null} try { $Params = @{ Uri = $Uri ConnectionTimeoutSeconds = 5 UserAgent = $UserAgent SkipHeaderValidation = $SkipHeaderValidation.IsPresent } if ($Cmdlet -eq 'Invoke-WebRequest') { $result.Output = Invoke-WebRequest @Params $result.Content = $result.Output.Content | ConvertFrom-Json } else { $result.Output = Invoke-RestMethod @Params # NOTE: $result.Output should already be a PSObject (Invoke-RestMethod converts the returned json automatically) # so simply reference $result.Output $result.Content = $result.Output } } catch { $result.Error = $_ } return $result } # This function calls Invoke-WebRequest with the given uri function ExecuteWebRequest { param ( [Parameter(Mandatory)] [string] $Uri, [switch] $UseBasicParsing ) $result = [PSObject]@{Output = $null; Error = $null; Content = $null} try { $result.Output = Invoke-WebRequest -Uri $Uri -UseBasicParsing:$UseBasicParsing.IsPresent $result.Content = $result.Output.Content } catch { $result.Error = $_ } return $result } [string] $debugEncodingPrefix = 'WebResponse content encoding: ' # This function calls Invoke-WebRequest with the given uri and # parses the verbose output to determine the encoding used for the content. function ExecuteRestMethod { param ( [Parameter(Mandatory)] [string] $Uri, [switch] $UseBasicParsing ) $result = @{Output = $null; Error = $null; Encoding = $null; Content = $null} $debugPreferenceSave = $DebugPreference $DebugPreference = 'Continue' try { $debugFile = Join-Path $TestDrive -ChildPath ExecuteRestMethod.debug.txt $result.Output = Invoke-RestMethod -Uri $Uri -UseBasicParsing:$UseBasicParsing.IsPresent 5>$debugFile $result.Content = $result.Output # Invoke-RestMethod does not return encoding as part of an object, only in debug output, so we parse the debug output for the purposes of verifying the test. # This debug output is defined in InvokeRestMethodCommand.Common.cs ProcessResponse() if (Test-Path -Path $debugFile) { $result.Debug = Get-Content -Path $debugFile foreach ($item in $result.Debug) { $line = $item.Trim() if ($line.StartsWith($debugEncodingPrefix)) { $encodingName = [int]::Parse($item.SubString($EncodingPrefix.Length).Split('CodePage: ')[1].Trim()) $result.Encoding = [System.Text.Encoding]::GetEncoding($encodingName) break } } if ($null -eq $result.Encoding) { throw "Encoding not found in debug output. Lines: $($result.Debug.Count) Content:$($result.Debug)" } } if ($null -eq $result.Debug) { throw "No debug output was found" } } catch { $result.Error = $_ | Select-Object * | Out-String } finally { $DebugPreference = $debugPreferenceSave if (Test-Path -Path $debugFile) { Remove-Item -Path $debugFile -ErrorAction SilentlyContinue } } return $result } function GetMultipartBody { param ( [Switch] $String, [Switch] $File ) $multipartContent = [System.Net.Http.MultipartFormDataContent]::new() if ($String.IsPresent) { $stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $stringHeader.Name = "TestString" $StringContent = [System.Net.Http.StringContent]::new("TestValue") $StringContent.Headers.ContentDisposition = $stringHeader $multipartContent.Add($stringContent) } if ($File.IsPresent) { $multipartFile = Join-Path $TestDrive 'multipart.txt' "TestContent" | Set-Content $multipartFile $FileStream = [System.IO.FileStream]::new($multipartFile, [System.IO.FileMode]::Open) $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $fileHeader.Name = "TestFile" $fileHeader.FileName = 'multipart.txt' $fileContent = [System.Net.Http.StreamContent]::new($FileStream) $fileContent.Headers.ContentDisposition = $fileHeader $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain") $multipartContent.Add($fileContent) } # unary comma required to prevent $multipartContent from being unwrapped/enumerated return , $multipartContent } <# Defines the list of redirect codes to test as well as the expected Method when the redirection is handled. See https://learn.microsoft.com/dotnet/api/system.net.httpstatuscode for additonal details. #> $redirectTests = @( @{redirectType = 'MultipleChoices'; redirectedMethod = 'GET'} @{redirectType = 'Ambiguous'; redirectedMethod = 'GET'} # Synonym for MultipleChoices @{redirectType = 'Moved'; redirectedMethod = 'GET'} @{redirectType = 'MovedPermanently'; redirectedMethod = 'GET'} # Synonym for Moved @{redirectType = 'Found'; redirectedMethod = 'GET'} @{redirectType = 'Redirect'; redirectedMethod = 'GET'} # Synonym for Found @{redirectType = 'redirectMethod'; redirectedMethod = 'GET'} @{redirectType = 'SeeOther'; redirectedMethod = 'GET'} # Synonym for RedirectMethod @{redirectType = 'TemporaryRedirect'; redirectedMethod = 'POST'} @{redirectType = 'RedirectKeepVerb'; redirectedMethod = 'POST'} # Synonym for TemporaryRedirect @{redirectType = 'relative'; redirectedMethod = 'GET'} ) Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener $NotFoundQuery = @{ statuscode = 404 responsephrase = 'Not Found' contenttype = 'text/plain' body = 'oops' headers = "{}" } } AfterAll { $ProgressPreference = $oldProgress } # Validate the output of Invoke-WebRequest # function ValidateResponse { param ($response) $response.Error | Should -Be $null # A successful call returns: Status = 200, and StatusDescription = "OK" $response.Output.StatusDescription | Should -Match "OK" $response.Output.StatusCode | Should -Be 200 # Make sure the response contains the following properties: $response.Output.RawContent | Should -Not -Be $null $response.Output.Headers | Should -Not -Be $null $response.Output.RawContent | Should -Not -Be $null $response.Output.RawContentLength | Should -Not -Be $null $response.Output.Content | Should -Not -Be $null } #User-Agent changes on different platforms, so tests should only be run if on the correct platform It "Invoke-WebRequest returns Correct User-Agent on MacOSX" -Skip:(!$IsMacOS) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.'User-Agent' | Should -MatchExactly '.*\(Macintosh;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-WebRequest returns Correct User-Agent on Linux" -Skip:(!$IsLinux) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.'User-Agent' | Should -MatchExactly '.*\(Linux;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-WebRequest returns Correct User-Agent on Windows" -Skip:(!$IsWindows) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.'User-Agent' | Should -MatchExactly '.*\(Windows NT \d+\.\d*;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-WebRequest returns headers dictionary" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Be $Uri.Authority } It "Invoke-WebRequest with blank ContentType succeeds" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri' -ContentType ''" $result = ExecuteWebCommand -command $command # Validate response ValidateResponse -response $result $result.Output.Headers.'Content-Length' | Should -BeNullOrEmpty } It "Validate Invoke-WebRequest -DisableKeepAlive" { # Operation options $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri $uri -DisableKeepAlive" $result = ExecuteWebCommand -command $command ValidateResponse -response $result $result.Output.Headers.Connection | Should -Be "Close" } It "Validate Invoke-WebRequest -HttpVersion '<httpVersion>'" -Skip:(!$IsWindows) -TestCases @( @{ httpVersion = '1.1'}, @{ httpVersion = '2'} ) { param($httpVersion) if(Test-IsWinServer2012R2 -and $httpVersion -eq '2') { Set-ItResult -Skipped -Because "HTTP/2 is not supported on Windows Server 2012R2" return } # Operation options $uri = Get-WebListenerUrl -Test 'Get' -Https $command = "Invoke-WebRequest -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.protocol | Should -Be "HTTP/$httpVersion" } It "Validate Invoke-WebRequest -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-WebRequest -Uri '$uri' -MaximumRedirection 4" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Match $uri.Authority } It "Validate Invoke-WebRequest error for -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-WebRequest -Uri '$uri' -MaximumRedirection 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Validate Invoke-WebRequest redirect with -Query destination Http" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{destination = $httpUri} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Match $httpUri.Authority } It "Validate Invoke-WebRequest redirect with -Query destination Https" { $httpsUri = Get-WebListenerUrl -Test 'Get' -Https $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpsUri} $command = "Invoke-WebRequest -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Match $httpsUri.Authority } It "Invoke-WebRequest supports request that returns page containing UTF-8 data." { $uri = Get-WebListenerUrl -Test 'Encoding' -TestValue 'Utf8' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result $Result.Output.Encoding.BodyName | Should -Be 'utf-8' $Result.Output.Content | Should -Match '⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌' } It "Invoke-WebRequest -ContentType overwrites Content-Type from -Headers." { $uri = Get-WebListenerUrl -Test 'POST' $command = "Invoke-WebRequest -Uri '$uri' -ContentType 'application/json' -Headers @{'Content-Type'='plain/text'} -Method 'POST'" $result = ExecuteWebCommand -command $command $result.Output.BaseResponse.RequestMessage.Content.Headers.ContentType.MediaType | Should -BeExactly 'application/json' } It "Invoke-WebRequest supports sending request as UTF-8." { $uri = Get-WebListenerUrl -Test 'POST' # Body must contain non-ASCII characters $command = "Invoke-WebRequest -Uri '$uri' -Body 'проверка' -ContentType 'application/json; charset=utf-8' -Method 'POST'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result $result.Output.Encoding.BodyName | Should -BeExactly 'utf-8' $object = $Result.Output.Content | ConvertFrom-Json $object.Data | Should -BeExactly 'проверка' } It "Invoke-WebRequest supports sending XML requests without encoding" { $uri = Get-WebListenerUrl -Test POST $body = '<?xml version="1.0"?><foo />' $result = Invoke-WebRequest -Uri $uri -body ([xml]$body) -ContentType 'text/xml' -method 'POST' $object = $result.Content | ConvertFrom-Json $object.Data | Should -BeExactly $body } It "Invoke-WebRequest supports request that returns page containing CodPage 936 data." { $uri = Get-WebListenerUrl -Test 'Encoding' -TestValue 'CP936' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result $Result.Output.Encoding.CodePage | Should -Be 936 $Result.Output.Content | Should -Match '测试123' } It "Invoke-WebRequest validate ConnectionTimeoutSeconds option" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '5' $command = "Invoke-WebRequest -Uri '$uri' -ConnectionTimeoutSeconds 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "ConnectionTimeoutReached,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Invoke-WebRequest validate TimeoutSec alias" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '5' $command = "Invoke-WebRequest -Uri '$uri' -TimeoutSec 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "ConnectionTimeoutReached,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Validate Invoke-WebRequest error with -Proxy and -NoProxy option" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '10' $command = "Invoke-WebRequest -Uri '$uri' -Proxy 'http://127.0.0.1:8080' -NoProxy -TimeoutSec 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "AmbiguousParameterSet,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } $testCase = @( @{ proxy_address = (Get-WebListenerUrl).Authority; name = 'HTTP proxy'; protocol = 'http' } @{ proxy_address = (Get-WebListenerUrl -https).Authority; name = 'HTTPS proxy'; protocol = 'https' } ) It "Validate Invoke-WebRequest with -Proxy option set - '<name>'" -TestCases $testCase { param($proxy_address, $name, $protocol) # use external url, but with proxy the external url should not actually be called $command = "Invoke-WebRequest -Uri ${protocol}://httpbin.org -Proxy '${protocol}://${proxy_address}'" $result = ExecuteWebCommand -command $command $command = "Invoke-WebRequest -Uri '${protocol}://${proxy_address}' -NoProxy" $expectedResult = ExecuteWebCommand -command $command $result.Output.Content | Should -BeExactly $expectedResult.Output.Content } # Perform the following operation for Invoke-WebRequest # gzip Returns gzip-encoded data. # deflate Returns deflate-encoded data. # brotli Returns brotli-encoded data. # $dataEncodings = @("Chunked", "Compress", "Deflate", "GZip", "Identity") # Note: These are the supported options, but we do not have a web service to test them all. It "Invoke-WebRequest supports request that returns <DataEncoding>-encoded data." -TestCases @( @{ DataEncoding = "gzip" } @{ DataEncoding = "deflate" } @{ DataEncoding = "brotli" } ) { param($dataEncoding) $uri = Get-WebListenerUrl -Test 'Compression' -TestValue $dataEncoding $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content # The content should be de-compressed, and otherwise converting from JSON will fail. $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.Headers.Host | Should -BeExactly $uri.Authority } # Perform the following operation for Invoke-WebRequest using the following content types: "text/plain", "application/xml", "application/xml" # post Returns POST data. # patch Returns PATCH data. # put Returns PUT data. # delete Returns DELETE data $testMethods = @("POST", "PATCH", "PUT", "DELETE") $contentTypes = @("text/plain", "application/xml", "application/json") foreach ($contentType in $contentTypes) { foreach ($method in $testMethods) { # Operation options $uri = Get-WebListenerUrl -Test $method $body = GetTestData -contentType $contentType $command = "Invoke-WebRequest -Uri $uri -Body '$body' -Method $method -ContentType $contentType" $commandNoContentType = "Invoke-WebRequest -Uri $uri -Body '$body' -Method $method" It "Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Body [body data]" { $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.url | Should -Match $uri $jsonContent.headers.'Content-Type' | Should -Match $contentType # Validate that the response Content.data field is the same as what we sent. $jsonContent.data | Should -Be $body } It "Invoke-WebRequest -Uri $uri -Method $method -Body [body data]" { $result = ExecuteWebCommand -command $commandNoContentType ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.url | Should -Match $uri if ($method -eq "POST") { $jsonContent.headers.'Content-Type' | Should -Match "application/x-www-form-urlencoded" # Validate that the response Content.form field is the same as what we sent. [string]$jsonContent.form | Should -Be ([string][PSCustomObject]@{$body.Split("=")[0] = [System.Object[]]}) $jsonContent.data | Should -BeNullOrEmpty } else { # Validate that the response Content.data field is the same as what we sent. $jsonContent.data | Should -Be $body } } } } It "Validate Invoke-WebRequest -Headers --> Set KeepAlive to false via headers" { $uri = Get-WebListenerUrl -Test 'Get' $result = ExecuteRequestWithHeaders -cmdletName Invoke-WebRequest -uri $uri ValidateResponse -response $result $result.Output.Headers.Connection | Should -Be "Close" } # Validate all available user agents for Invoke-WebRequest $agents = @{ InternetExplorer = "MSIE 9.0" Chrome = "Chrome" Opera = "Opera" Safari = "Safari" FireFox = "Firefox" } foreach ($agentName in $agents.Keys) { $expectedAgent = $agents[$agentName] $uri = Get-WebListenerUrl -Test 'Get' $userAgent = "[Microsoft.PowerShell.Commands.PSUserAgent]::$agentName" $command = "Invoke-WebRequest -Uri $uri -UserAgent ($userAgent) " It "Validate Invoke-WebRequest UserAgent. Execute--> $command" { $result = ExecuteWebCommand -command $command ValidateResponse -response $result # Validate response content $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Be $uri.Authority $jsonContent.headers.'User-Agent' | Should -Match $expectedAgent } } It "Validate Invoke-WebRequest -OutFile" { $uri = Get-WebListenerUrl -Test 'Get' $result = ExecuteRequestWithOutFile -cmdletName "Invoke-WebRequest" -uri $uri $jsonContent = $result.Output | ConvertFrom-Json $jsonContent.headers.Host | Should -Be $uri.Authority } It "Invoke-WebRequest -OutFile folder Downloads the file and names it" { $uri = Get-WebListenerUrl -Test 'Get' $content = Invoke-WebRequest -Uri $uri $outFile = Join-Path $TestDrive $content.BaseResponse.RequestMessage.RequestUri.Segments[-1] # ensure the file does not exist Remove-Item -Force -ErrorAction Ignore -Path $outFile Invoke-WebRequest -Uri $uri -OutFile $TestDrive Test-Path $outFile | Should -Be $true Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $content.Content.Length } It "Invoke-WebRequest -PassThru -OutFile <directory> saves the downloaded file path as a property in WebResponseObject"{ $uri = Get-WebListenerUrl -Test 'Get' $content = Invoke-WebRequest -Uri $uri -PassThru -OutFile $TestDrive $content.OutFile | Should -exist $content.OutFile | Should -Be (Join-Path $TestDrive 'Get') } It "Invoke-WebRequest -PassThru -OutFile <file> saves the downloaded file path as a property in WebResponseObject"{ $uri = Get-WebListenerUrl -Test 'Get' $filePath = Join-Path $TestDrive "pestertest-outfile" $content = Invoke-WebRequest -Uri $uri -PassThru -OutFile $filePath $content.OutFile | Should -exist $content.OutFile | Should -Be $filePath } It "Invoke-WebRequest -PassThru -OutFile -Verbose File Name reflects the downloaded file name" { $uri = Get-WebListenerUrl -Test 'Get' $filePath = Join-Path $TestDrive "pestertest-outfile" $content = Invoke-WebRequest -Verbose -Uri $uri -PassThru -OutFile $filePath 4>variable:verbo $verbo[-1].Message | Should -Match "pestertest-outfile" } It "Invoke-WebRequest should fail if -OutFile is <Name>." -TestCases @( @{ Name = "empty"; Value = [string]::Empty } @{ Name = "null"; Value = $null } ) { param ($value) $uri = Get-WebListenerUrl -Test 'Get' $errorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" { Invoke-WebRequest -Uri $uri -OutFile $value} | Should -Throw -ErrorId $errorId } It "Validate Invoke-WebRequest handles missing Content-Type in response header" { #Validate that exception is not thrown when response headers are missing Content-Type. $uri = Get-WebListenerUrl -Test 'ResponseHeaders' -Query @{'Content-Type' = ''} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty } It "Validate Invoke-WebRequest StandardMethod and CustomMethod parameter sets" { $uri = Get-WebListenerUrl -Test 'Get' #Validate that parameter sets are functioning correctly $errorId = "AmbiguousParameterSet,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" { Invoke-WebRequest -Uri $uri -Method GET -CustomMethod TEST } | Should -Throw -ErrorId $errorId } It "Validate Invoke-WebRequest CustomMethod method is used" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri' -CustomMethod TEST" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty ($result.Output.Content | ConvertFrom-Json).method | Should -Be "TEST" } It "Validate Invoke-WebRequest default ContentType for CustomMethod POST" { $uri = Get-WebListenerUrl -Test 'Post' $command = "Invoke-WebRequest -Uri '$uri' -CustomMethod POST -Body 'testparam=testvalue'" $result = ExecuteWebCommand -command $command $jsonResult = $result.Output.Content | ConvertFrom-Json $jsonResult.form.testparam | Should -Be "testvalue" $jsonResult.Headers.'Content-Type' | Should -Be "application/x-www-form-urlencoded" } It "Validate Invoke-WebRequest body is converted to query params for CustomMethod GET" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri' -CustomMethod GET -Body @{'testparam'='testvalue'}" $result = ExecuteWebCommand -command $command ($result.Output.Content | ConvertFrom-Json).args.testparam | Should -Be "testvalue" } It 'Validate Invoke-WebRequest empty body CustomMethod GET' { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri' -CustomMethod GET" $result = ExecuteWebCommand -command $command $result.Output.Headers.'Content-Length' | Should -BeNullOrEmpty } It "Validate Invoke-WebRequest body is converted to query params for CustomMethod GET and -NoProxy" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri' -CustomMethod GET -Body @{'testparam'='testvalue'} -NoProxy" $result = ExecuteWebCommand -command $command ($result.Output.Content | ConvertFrom-Json).query | Should -Be "?testparam=testvalue" } It "Validate Invoke-WebRequest returns HTTP errors in exception" { $query = @{ body = "I am a teapot!!!" statuscode = 418 responsephrase = "I am a teapot" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Error.ErrorDetails.Message | Should -Be $query.body $result.Error.Exception | Should -BeOfType Microsoft.PowerShell.Commands.HttpResponseException $result.Error.Exception.Response.StatusCode | Should -Be 418 $result.Error.Exception.Response.ReasonPhrase | Should -Be $query.responsephrase $result.Error.Exception.Message | Should -Match ": 418 \($($query.responsephrase)\)\." $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Validate Invoke-WebRequest returns <type> errors in exception" -TestCases @( @{ type = "XML" query = @{ contenttype = 'application/xml' body = '<?xml version="1.0" encoding="UTF-8"?><Error><Code>418</Code><Message>I am a teapot!!!</Message></Error>' statuscode = 418 responsephrase = "I am a teapot" } expectederror = $IsWindows ? "`r`n<Error>`r`n <Code>418</Code>`r`n <Message>I am a teapot!!!</Message>`r`n</Error>" : "`n<Error>`n <Code>418</Code>`n <Message>I am a teapot!!!</Message>`n</Error>" } @{ type = "Json" query = @{ contenttype = 'application/json' body = '{"error":{"code":"418", "message":"I am a teapot!!!"}}' statuscode = 418 responsephrase = "I am a teapot" } expectederror = $IsWindows ? "`r`n{`r`n `"error`": {`r`n `"code`": `"418`",`r`n `"message`": `"I am a teapot!!!`"`r`n }`r`n}" : "`n{`n `"error`": {`n `"code`": `"418`",`n `"message`": `"I am a teapot!!!`"`n }`n}" } ) { param($query, $expectederror) $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Error.ErrorDetails.Message | Should -Be $expectederror } It "Validate Invoke-WebRequest returns empty RelationLink property if there is no Link Header" { $uri = $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink.Count | Should -Be 0 } It "Validate Invoke-WebRequest returns valid RelationLink property with absolute uris if Link Header is present <name>" -TestCases @( $originalUri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = 5; linknumber = 2} @{name = '(URI with scheme)'; uri = $originalUri} @{name = '(URI without scheme)'; uri = $originalUri.OriginalString.Split("//")[1]} ) { param($uri) $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink.Count | Should -BeExactly 5 $baseUri = Get-WebListenerUrl -Test 'Link' $result.Output.RelationLink["next"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=3&type=default" $result.Output.RelationLink["last"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=5&type=default" $result.Output.RelationLink["prev"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=default" $result.Output.RelationLink["first"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=default" $result.Output.RelationLink["self"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=2&type=default" } It "Validate Invoke-WebRequest returns valid RelationLink property with absolute uris if Multiple Link Headers are present" { $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = 5; linknumber = 2; type = 'multiple'} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink.Count | Should -BeExactly 5 $baseUri = Get-WebListenerUrl -Test 'Link' $result.Output.RelationLink["next"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=3&type=multiple" $result.Output.RelationLink["last"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=5&type=multiple" $result.Output.RelationLink["prev"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=multiple" $result.Output.RelationLink["first"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=multiple" $result.Output.RelationLink["self"] | Should -BeExactly "${baseUri}?maxlinks=5&linknumber=2&type=multiple" } It "Validate Invoke-WebRequest RelationLink keys are treated case-insensitively" { $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = 5; linknumber = 2; type = 'multiple'} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink["next"] | Should -Not -BeNullOrEmpty $result.Output.RelationLink["next"] | Should -BeExactly $result.Output.RelationLink["Next"] $result.Output.RelationLink["last"] | Should -Not -BeNullOrEmpty $result.Output.RelationLink["last"] | Should -BeExactly $result.Output.RelationLink["LAST"] $result.Output.RelationLink["prev"] | Should -Not -BeNullOrEmpty $result.Output.RelationLink["prev"] | Should -BeExactly $result.Output.RelationLink["preV"] $result.Output.RelationLink["first"] | Should -Not -BeNullOrEmpty $result.Output.RelationLink["first"] | Should -BeExactly $result.Output.RelationLink["FiRsT"] $result.Output.RelationLink["self"] | Should -Not -BeNullOrEmpty $result.Output.RelationLink["self"] | Should -BeExactly $result.Output.RelationLink["self"] } It "Validate Invoke-WebRequest quietly ignores invalid Link Headers in RelationLink property: <type>" -TestCases @( @{ type = "noUrl" } @{ type = "malformed" } @{ type = "noRel" } ) { param($type) $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink.Count | Should -BeExactly 3 $baseUri = Get-WebListenerUrl -Test 'Link' $result.Output.RelationLink["last"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=3&type=${type}" $result.Output.RelationLink["first"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" $result.Output.RelationLink["self"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" } It "Validate Invoke-WebRequest handles different whitespace for Link Headers: <type>" -TestCases @( @{ type = "noWhitespace" } @{ type = "extraWhitespace" } ) { param($type) $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.RelationLink.Count | Should -BeExactly 4 $baseUri = Get-WebListenerUrl -Test 'Link' $result.Output.RelationLink["last"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=3&type=${type}" $result.Output.RelationLink["first"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" $result.Output.RelationLink["self"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" $result.Output.RelationLink["next"] | Should -BeExactly "${baseUri}?maxlinks=3&linknumber=2&type=${type}" } It "Verify Invoke-WebRequest supresses terminating errors with -SkipHttpErrorCheck" { $uri = Get-WebListenerUrl -Test 'Response' -Query $NotFoundQuery $command = "Invoke-WebRequest -SkipHttpErrorCheck -Uri '$uri'" $result = ExecuteWebCommand -Command $command $result.output.StatusCode | Should -Be 404 $result.output.Content | Should -BeExactly "oops" $result.error | Should -BeNullOrEmpty } It "Verify Invoke-WebRequest terminates without -SkipHttpErrorCheck" { $uri = Get-WebListenerUrl -Test 'Response' -Query $NotFoundQuery $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -Command $command $result.output | Should -BeNullOrEmpty $result.error | Should -Not -BeNullOrEmpty } Context "Redirect" { It "Validates Invoke-WebRequest with -PreserveAuthorizationOnRedirect preserves the authorization header on redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Uri $uri -PreserveAuthorizationOnRedirect $response.Error | Should -BeNullOrEmpty $response.Content.Headers."Authorization" | Should -BeExactly "test" } It "Validates Invoke-WebRequest with -PreserveAuthorizationOnRedirect respects -MaximumRedirection on redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Uri $uri -PreserveAuthorizationOnRedirect -MaximumRedirection 2 $response.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Validates Invoke-WebRequest preserves the authorization header on multiple redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue 3 -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Uri $uri -PreserveAuthorizationOnRedirect $response.Error | Should -BeNullOrEmpty $response.Content.Headers."Authorization" | Should -BeExactly "test" } It "Validates Invoke-WebRequest with -WebSession and -PreserveAuthorizationOnRedirect doesn't change session variable on multiple redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) #[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")] $token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force $credential = [pscredential]::new("testuser", $token) $certificate = Get-WebListenerClientCertificate $headers = @{"Authorization" = "test"} $proxy = (Get-WebListenerUrl).Authority $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue 2 -Query @{type = $redirectType} $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.MaximumRedirection = 2 $session.MaximumRetryCount = 2 $session.RetryIntervalInSeconds = 2 $session.UseDefaultCredentials = $true $null = Invoke-WebRequest -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -AllowUnencryptedAuthentication -Headers $headers $session.MaximumRedirection | Should -BeExactly 2 $session.MaximumRetryCount | Should -BeExactly 2 $session.RetryIntervalInSeconds | Should -BeExactly 2 $session.UseDefaultCredentials | Should -BeExactly $true $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.Credentials = $credential $session.Certificates = [System.Security.Cryptography.X509Certificates.X509CertificateCollection]::new([X509Certificate]$certificate) $null = Invoke-WebRequest -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -SkipCertificateCheck -Headers $headers $session.Credentials.UserName | Should -BeExactly $credential.UserName $session.Credentials.Password | Should -BeExactly $credential.GetNetworkCredential().Password $session.Certificates.Thumbprint | Should -BeExactly $certificate.Thumbprint $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.Proxy = [System.Net.WebProxy]::new($proxy) $null = Invoke-WebRequest -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -Headers $headers $session.Proxy.GetProxy($uri).Authority | Should -BeExactly $proxy } It "Validates Invoke-WebRequest strips the authorization header on various redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Uri $uri $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been removed. $response.Content.Headers."Authorization" | Should -BeNullOrEmpty } # NOTE: Only testing redirection of POST -> GET for unique underlying values of HttpStatusCode. # Some names overlap in underlying value. It "Validates Invoke-WebRequest strips the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been removed. $response.Content.Headers."Authorization" | Should -BeNullOrEmpty # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-WebRequest -PreserveAuthorizationOnRedirect keeps the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been preserved. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-WebRequest -PreserveAuthorizationOnRedirect -CustomMethod POST keeps the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -Uri $uri -CustomMethod 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been preserved. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-WebRequest -PreserveHttpMethodOnRedirect keeps the authorization header redirects and do remains POST when it handles the redirect: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been kept. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST doesn't change. $response.Content.Method | Should -Be 'POST' } It "Validates Invoke-WebRequest handles responses without Location header for requests with Authorization header and redirect: <redirectType>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) # Skip relative test as it is not a valid response type. if ($redirectType -eq 'relative') { return } # When an Authorization request header is present, # and -PreserveAuthorizationOnRedirect is not present, # PowerShell should throw an HTTP Response Exception # for a redirect response which does not contain a Location response header. # The correct redirect status code should be included in the exception. $StatusCode = [int][System.Net.HttpStatusCode]$redirectType $uri = Get-WebListenerUrl -Test Response -Query @{statuscode = $StatusCode} $command = "Invoke-WebRequest -Uri '$uri' -Headers @{Authorization = 'foo'}" $response = ExecuteWebCommand -command $command $response.Error.Exception | Should -BeOfType Microsoft.PowerShell.Commands.HttpResponseException $response.Error.Exception.Response.StatusCode | Should -Be $StatusCode $response.Error.Exception.Response.Headers.Location | Should -BeNullOrEmpty } It "Validate Invoke-WebRequest Https to Http redirect with -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri} $command = "Invoke-WebRequest -Uri '$uri' -SkipCertificateCheck -AllowInsecureRedirect" $result = ExecuteWebCommand -command $command $jsonContent = $result.Output.Content | ConvertFrom-Json $jsonContent.headers.Host | Should -Match $httpUri.Authority } It "Validate Invoke-WebRequest Https to Http redirect without -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri} $command = "Invoke-WebRequest -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "InsecureRedirection,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Validate Invoke-WebRequest Https to Http (No Scheme) redirect without -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri.Authority} $command = "Invoke-WebRequest -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } } Context "Invoke-WebRequest SkipHeaderVerification Tests" { BeforeAll { $Testfile = Join-Path $testdrive 'SkipHeaderVerification.txt' 'bar' | Set-Content $Testfile } It "Verifies Invoke-WebRequest default header handling with no errors" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "*"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers $response.Error | Should -BeNullOrEmpty $response.Content.Headers."If-Match" | Should -BeExactly "*" } It "Verifies Invoke-WebRequest default header handling reports an error is returned for an invalid If-Match header value" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "12345"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers $response.Error | Should -Not -BeNullOrEmpty $response.Error.FullyQualifiedErrorId | Should -Be "System.FormatException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" $response.Error.Exception.Message | Should -Be "The format of value '12345' is invalid." } It "Verifies Invoke-WebRequest header handling does not report an error when using -SkipHeaderValidation" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "12345"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers -SkipHeaderValidation $response.Error | Should -BeNullOrEmpty $response.Content.Headers."If-Match" | Should -BeExactly "12345" } It "Verifies Invoke-WebRequest default UserAgent handling with no errors" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -Cmdlet "Invoke-WebRequest" $response.Error | Should -BeNullOrEmpty $Pattern = [regex]::Escape($UserAgent) $response.Content.Headers."User-Agent" | Should -Match $Pattern } It "Verifies Invoke-WebRequest default UserAgent handling reports an error is returned for an invalid UserAgent value" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = 'Invalid:Agent' $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -Cmdlet "Invoke-WebRequest" $response.Error | Should -Not -BeNullOrEmpty $response.Error.FullyQualifiedErrorId | Should -Be "System.FormatException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" $response.Error.Exception.Message | Should -Be "The format of value 'Invalid:Agent' is invalid." } It "Verifies Invoke-WebRequest UserAgent handling does not report an error when using -SkipHeaderValidation" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = 'Invalid:Agent' $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -SkipHeaderValidation -Cmdlet "Invoke-WebRequest" $response.Error | Should -BeNullOrEmpty $Pattern = [regex]::Escape($UserAgent) $response.Content.Headers."User-Agent" | Should -Match $Pattern } It "Verifies Invoke-WebRequest default ContentType handling reports no error is returned for a valid Content-Type header value and -Body" { $contentType = 'text/plain' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' $response = Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -Body $body $result = $response.Content | ConvertFrom-Json $result.data | Should -BeExactly $body $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-WebRequest default ContentType handling reports an error is returned for an invalid Content-Type header value and -Body" { $contentType = 'foo' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' { Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -Body $body -ErrorAction 'Stop' } | Should -Throw -ErrorId "WebCmdletContentTypeException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest ContentType handling reports no error is returned for an invalid Content-Type header value, -Body, and -SkipHeaderValidation" { $contentType = 'foo' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' $response = Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -Body $body -SkipHeaderValidation $result = $response.Content | ConvertFrom-Json $result.data | Should -BeExactly 'bar' $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-WebRequest default ContentType handling reports no error is returned for a valid Content-Type header value and -InFile" { $contentType = 'text/plain' $uri = Get-WebListenerUrl -Test 'Post' $response = Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile $result = $response.Content | ConvertFrom-Json # Match used due to inconsistent newline rendering $result.data | Should -Match 'bar' $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-WebRequest default ContentType handling reports an error is returned for an invalid Content-Type header value and -InFile" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Post' { Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile -ErrorAction 'Stop' } | Should -Throw -ErrorId "WebCmdletContentTypeException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest default ContentType handling reports no error is returned for an invalid Content-Type header value, -Infile, and -SkipHeaderValidation" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Post' $response = Invoke-WebRequest -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile -SkipHeaderValidation $result = $response.Content | ConvertFrom-Json # Match used due to inconsistent newline rendering $result.data | Should -Match 'bar' $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-WebRequest applies -ContentType when no -Body is present" { $contentType = 'application/json' $uri = Get-WebListenerUrl -Test 'Get' $response = Invoke-WebRequest -Uri $uri -Method 'GET' -ContentType $contentType $result = $response.Content | ConvertFrom-Json $result.data | Should -BeNullOrEmpty $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-WebRequest applies an invalid -ContentType when no -Body is present and -SkipHeaderValidation is present" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Get' $response = Invoke-WebRequest -Uri $uri -Method 'GET' -ContentType $contentType -SkipHeaderValidation $result = $response.Content | ConvertFrom-Json $result.data | Should -BeNullOrEmpty $result.headers.'Content-Type' | Should -BeExactly $contentType } } #region charset encoding tests Context "BasicHtmlWebResponseObject Encoding tests" { It "Verifies Invoke-WebRequest detects charset meta value when the ContentType header does not define it." { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset="Unicode"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest detects charset meta value when newlines are encountered in the element." { $query = @{ contenttype = 'text/html' body = "<html>`n <head>`n <meta`n charset=`"Unicode`"`n >`n </head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest detects charset meta value when the attribute value is unquoted." { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset = Unicode></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest detects http-equiv charset meta value when the ContentType header does not define it." { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta http-equiv=`"content-type`" content=`"text/html; charset=Unicode`">`n</head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest detects http-equiv charset meta value newlines are encountered in the element." { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta`n http-equiv=`"content-type`"`n content=`"text/html; charset=Unicode`">`n</head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest ignores meta charset value when Content-Type header defines it." { $query = @{ contenttype = 'text/html; charset=utf-8' body = '<html><head><meta charset="utf-32"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query # NOTE: meta charset should be ignored $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest honors non-utf8 charsets in the Content-Type header" { $query = @{ contenttype = 'text/html; charset=utf-16' body = '<html><head><meta charset="utf-32"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query # NOTE: meta charset should be ignored $expectedEncoding = [System.Text.Encoding]::GetEncoding('utf-16') $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest defaults to iso-UTF-8 when an unsupported/invalid charset is declared" { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset="invalid"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } It "Verifies Invoke-WebRequest defaults to UTF-8 when an unsupported/invalid charset is declared using http-equiv" { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta http-equiv=`"content-type`" content=`"text/html; charset=Invalid`">`n</head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteWebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Output.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName $response.Output | Should -BeOfType Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject } } #endregion charset encoding tests #region Content Header Inclusion Context "Content Header" { It "Verifies Invoke-WebRequest includes Content headers in Headers property" { $query = @{ contenttype = 'text/plain' body = 'OK' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse $result $result.Output.Headers.'Content-Type' | Should -Be 'text/plain' $result.Output.Headers.'Content-Length' | Should -Be 2 } It "Verifies Invoke-WebRequest includes Content headers in RawContent property" { $query = @{ contenttype = 'text/plain' body = 'OK' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse $result $result.Output.RawContent | Should -Match ([regex]::Escape('Content-Type: text/plain')) $result.Output.RawContent | Should -Match ([regex]::Escape('Content-Length: 2')) } It "Verifies Invoke-WebRequest Supports Multiple response headers with same name" { $query = @{ contenttype = 'text/plain' body = 'OK' headers = @{ 'X-Fake-Header' = @('testvalue01', 'testvalue02') } | ConvertTo-Json -Compress } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command ValidateResponse $result $result.Output.Headers.'X-Fake-Header'.Count | Should -Be 2 $result.Output.Headers.'X-Fake-Header'.Contains('testvalue01') | Should -BeTrue $result.Output.Headers.'X-Fake-Header'.Contains('testvalue02') | Should -BeTrue $result.Output.RawContent | Should -Match ([regex]::Escape('X-Fake-Header: testvalue01')) $result.Output.RawContent | Should -Match ([regex]::Escape('X-Fake-Header: testvalue02')) } It "Verifies Invoke-WebRequest does not sent expect 100-continue headers by default" { $uri = Get-WebListenerUrl -Test 'Get' $response = Invoke-WebRequest -Uri $uri $result = $response.Content | ConvertFrom-Json $result.headers.Expect | Should -BeNullOrEmpty $result.method | Should -BeExactly "GET" $result.url | Should -BeExactly $uri.ToString() } It "Verifies Invoke-WebRequest sends expect 100-continue header when defined in -Headers" { $uri = Get-WebListenerUrl -Test 'Get' $response = Invoke-WebRequest -Uri $uri -Headers @{Expect = '100-continue'} $result = $response.Content | ConvertFrom-Json $result.headers.Expect | Should -BeExactly '100-continue' $result.method | Should -BeExactly "GET" $result.url | Should -BeExactly $uri.ToString() } } #endregion Content Header Inclusion Context "HTTPS Tests" { It "Validate Invoke-WebRequest -SkipCertificateCheck" { # validate that exception is thrown for URI with expired certificate $Uri = Get-WebListenerUrl -Https $command = "Invoke-WebRequest -Uri '$Uri'" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" # validate that no exception is thrown for URI with expired certificate when using -SkipCertificateCheck option $Uri = Get-WebListenerUrl -Https $command = "Invoke-WebRequest -Uri '$Uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty } It "Validate Invoke-WebRequest returns native HTTPS error message in exception" { $uri = Get-WebListenerUrl -Https $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command # need to check against inner exception since Linux and Windows uses different HTTP client libraries so errors aren't the same $result.Error.ErrorDetails.Message | Should -Match $result.Error.Exception.InnerException.Message $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest Certificate Authentication Fails without -Certificate" { $uri = Get-WebListenerUrl -Https -Test 'Cert' $result = Invoke-WebRequest -Uri $uri -SkipCertificateCheck | Select-Object -ExpandProperty Content | ConvertFrom-Json $result.Status | Should -Be 'FAILED' } It "Verifies Invoke-WebRequest Certificate Authentication Successful with -Certificate" { $uri = Get-WebListenerUrl -Https -Test 'Cert' $certificate = Get-WebListenerClientCertificate $result = Invoke-WebRequest -Uri $uri -Certificate $certificate -SkipCertificateCheck | Select-Object -ExpandProperty Content | ConvertFrom-Json $result.Status | Should -Be 'OK' $result.Thumbprint | Should -Be $certificate.Thumbprint } } Context "Multipart/form-data Tests" { <# Content-Type request headers for multipart/form-data appear as: multipart/form-data; boundary="0ab0cb90-f01b-4c15-bd4d-53d073efcc1d" MultipartFormDataContent sets a random GUID for the boundary before submitting the request to the remote endpoint. Tests in this context for Content-Type match 'multipart/form-data' as we do not have access to the random GUID. #> <# Kestrel/ASP.NET inconsistently renders the new line for uploaded text files. File content tests in this context use match as a workaround. #> BeforeAll { $file1Name = "testfile1.txt" $file1Path = Join-Path $testdrive $file1Name $file1Contents = "Test123" $file1Contents | Set-Content $file1Path -Force $file2Name = "testfile2.txt" $file2Path = Join-Path $testdrive $file2Name $file2Contents = "Test456" $file2Contents | Set-Content $file2Path -Force $file3Name = "Kündigung_Mustermann_Max.TTA_2023_01_30.txt" $file3Path = Join-Path $testdrive $file3Name $file3Contents = "Test789" $file3Contents | Set-Content $file3Path -Force } It "Verifies Invoke-WebRequest Supports Multipart String Values" { $body = GetMultipartBody -String $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString[0] | Should -Be 'TestValue' } It "Verifies Invoke-WebRequest Supports Multipart File Values" { $body = GetMultipartBody -File $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files[0].FileName | Should -Be 'multipart.txt' $result.Files[0].ContentType | Should -Be 'text/plain' $result.Files[0].Content | Should -Match 'TestContent' } It "Verifies Invoke-WebRequest Supports Mixed Multipart String and File Values" { $body = GetMultipartBody -String -File $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString[0] | Should -Be 'TestValue' $result.Files[0].FileName | Should -Be 'multipart.txt' $result.Files[0].ContentType | Should -Be 'text/plain' $result.Files[0].Content | Should -Match 'TestContent' } It "Verifies Invoke-WebRequest -Form supports string values" { $form = @{TestString = "TestValue"} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString.Count | Should -Be 1 $result.Items.TestString[0] | Should -BeExactly 'TestValue' } It "Verifies Invoke-WebRequest -Form supports a collection of string values" { $form = @{TestStrings = "TestValue", "TestValue2"} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestStrings.Count | Should -Be 2 $result.Items.TestStrings[0] | Should -BeExactly 'TestValue' $result.Items.TestStrings[1] | Should -BeExactly 'TestValue2' } It "Verifies Invoke-WebRequest -Form supports file values" { $form = @{TestFile = [System.IO.FileInfo]$file1Path} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].Name | Should -BeExactly "TestFile" $result.Files[0].FileName | Should -BeExactly $file1Name $result.Files[0].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents } It "Verifies Invoke-WebRequest -Form supports read-only file values" { # Create a read-only test file $readOnlyFile = Join-Path $TestDrive "readonly-test.txt" "ReadOnly test content" | Out-File -FilePath $readOnlyFile -Encoding utf8 Set-ItemProperty -Path $readOnlyFile -Name IsReadOnly -Value $true $form = @{TestFile = [System.IO.FileInfo]$readOnlyFile} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].Name | Should -BeExactly "TestFile" $result.Files[0].FileName | Should -BeExactly "readonly-test.txt" $result.Files[0].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[0].Content | Should -Match "ReadOnly test content" } It "Verifies Invoke-WebRequest -Form sets Content-Disposition FileName and FileNameStar." { $ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("attachment") $ContentDisposition.FileName = $fileName $ContentDisposition.FileNameStar = $fileName $form = @{TestFile = [System.IO.FileInfo]$file3Path} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].ContentDisposition.FileName | Should -Be $ContentDisposition.FileName $result.Files[0].ContentDisposition.FileNameStar | Should -Be $ContentDisposition.FileNameStar } It "Verifies Invoke-WebRequest -Form supports a collection of file values" { $form = @{TestFiles = [System.IO.FileInfo]$file1Path, [System.IO.FileInfo]$file2Path} $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 2 $result.Files[0].Name | Should -BeExactly "TestFiles" $result.Files[0].FileName | Should -BeExactly $file1Name $result.Files[0].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents $result.Files[1].Name | Should -BeExactly "TestFiles" $result.Files[1].FileName | Should -BeExactly $file2Name $result.Files[1].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[1].Content | Should -Match $file2Contents } It "Verifies Invoke-WebRequest -Form supports combinations of strings and files" { $form = @{ TestStrings = "TestValue", "TestValue2" TestFiles = [System.IO.FileInfo]$file1Path, [System.IO.FileInfo]$file2Path } $uri = Get-WebListenerUrl -Test 'Multipart' $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' $result = $response.Content | ConvertFrom-Json $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestStrings.Count | Should -Be 2 $result.Files.Count | Should -Be 2 $result.Items.TestStrings[0] | Should -BeExactly 'TestValue' $result.Items.TestStrings[1] | Should -BeExactly 'TestValue2' $result.Files[0].Name | Should -Be "TestFiles" $result.Files[0].FileName | Should -BeExactly $file1Name $result.Files[0].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents $result.Files[1].Name | Should -BeExactly "TestFiles" $result.Files[1].FileName | Should -BeExactly $file2Name $result.Files[1].ContentType | Should -BeExactly 'application/octet-stream' $result.Files[1].Content | Should -Match $file2Contents } It "Verifies Invoke-WebRequest -Form is mutually exclusive with -Body" { $form = @{TestString = "TestValue"} $body = "test" $uri = Get-WebListenerUrl -Test 'Multipart' {Invoke-WebRequest -Uri $uri -Form $form -Body $Body -ErrorAction 'Stop'} | Should -Throw -ErrorId 'WebCmdletBodyFormConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } It "Verifies Invoke-WebRequest -Form is mutually exclusive with -InFile" { $form = @{TestString = "TestValue"} $uri = Get-WebListenerUrl -Test 'Multipart' {Invoke-WebRequest -Uri $uri -Form $form -InFile $file1Path -ErrorAction 'Stop'} | Should -Throw -ErrorId 'WebCmdletFormInFileConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } } Context "Invoke-WebRequest -Authentication tests" { BeforeAll { #[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")] $token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force $credential = [pscredential]::new("testuser", $token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} ) } It "Verifies Invoke-WebRequest -Authentication Basic" { $params = @{ Uri = $httpsUri Authentication = "Basic" Credential = $credential SkipCertificateCheck = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } It "Verifies Invoke-WebRequest -Authentication Basic with null username" { $credential = [pscredential]::new([PSCustomObject]@{UserName = $null;Password=$token.psobject.BaseObject}) $params = @{ Uri = $httpsUri Authentication = "Basic" Credential = $credential SkipCertificateCheck = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Basic OnRlc3RwYXNzd29yZA==" } It "Verifies Invoke-WebRequest -Authentication <Authentication>" -TestCases $testCases { param($Authentication) $params = @{ Uri = $httpsUri Authentication = $Authentication Token = $token SkipCertificateCheck = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Bearer testpassword" } It "Verifies Invoke-WebRequest -Authentication does not support -UseDefaultCredentials" { $params = @{ Uri = $httpsUri Token = $token Authentication = "OAuth" UseDefaultCredentials = $true ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAuthenticationConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest -Authentication does not support Both -Credential and -Token" { $params = @{ Uri = $httpsUri Token = $token Credential = $credential Authentication = "OAuth" ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAuthenticationTokenConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest -Authentication <Authentication> requires -Token" -TestCases $testCases { param($Authentication) $params = @{ Uri = $httpsUri Authentication = $Authentication ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAuthenticationTokenNotSuppliedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest -Authentication Basic requires -Credential" { $params = @{ Uri = $httpsUri Authentication = "Basic" ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAuthenticationCredentialNotSuppliedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest -Authentication Requires HTTPS" { $params = @{ Uri = $httpUri Token = $token Authentication = "OAuth" ErrorAction = 'Stop' } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest -Authentication Can use HTTP with -AllowUnencryptedAuthentication" { $params = @{ Uri = $httpUri Token = $token Authentication = "OAuth" AllowUnencryptedAuthentication = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Bearer testpassword" } It "Verifies Invoke-WebRequest Negotiated -Credential over HTTPS" { $params = @{ Uri = $httpsBasicUri Credential = $credential SkipCertificateCheck = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } It "Verifies Invoke-WebRequest Negotiated -Credential Requires HTTPS" { $params = @{ Uri = $httpBasicUri Credential = $credential ErrorAction = 'Stop' } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } It "Verifies Invoke-WebRequest Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { $params = @{ Uri = $httpBasicUri Credential = $credential AllowUnencryptedAuthentication = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } # UseDefaultCredentials is only reliably testable on Windows It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials with '<AuthType>' over HTTPS" -Skip:$(!$IsWindows) -TestCases @( @{AuthType = 'NTLM'} @{AuthType = 'Negotiate'} ) { param($AuthType) $params = @{ Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType -Https UseDefaultCredentials = $true SkipCertificateCheck = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -Match "^$AuthType " } # The error condition can at least be tested on all platforms. It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials Requires HTTPS" { $params = @{ Uri = $httpUri UseDefaultCredentials = $true ErrorAction = 'Stop' } { Invoke-WebRequest @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" } # UseDefaultCredentials is only reliably testable on Windows It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials with '<AuthType>' Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) -TestCases @( @{AuthType = 'NTLM'} @{AuthType = 'Negotiate'} ) { param($AuthType) $params = @{ Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType UseDefaultCredentials = $true AllowUnencryptedAuthentication = $true } $Response = Invoke-WebRequest @params $result = $response.Content | ConvertFrom-Json $result.Headers.Authorization | Should -Match "^$AuthType " } } Context "Invoke-WebRequest -SslProtocol Test" { BeforeAll { # We put Tls13 tests at pending due to modern OS limitations. # Tracking issue https://github.com/PowerShell/PowerShell/issues/13439 $skipForTls1 = $true ## Test cases for the 1st 'It' $testCases1 = @( @{ Test = @{SslProtocol = 'Default'; ActualProtocol = 'Default'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls11'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls13'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12, Tls13'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12, Tls13'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls13'; ActualProtocol = 'Tls'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } # Skipping intermediary protocols is not supported on all platforms # Removed this as Tls now default to Tls12 # @{ Test = @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls'}; Pending = -not $IsWindows } @{ Test = @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls12'}; Pending = -not $IsWindows } ) $testCases2 = @( @{ Test = @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls13'; ActualProtocol = 'Tls'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls11, Tls12, Tls13'; ActualProtocol = 'Tls'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls13'}; Pending = $true } ) } foreach ($entry in $testCases1) { It "Verifies Invoke-WebRequest -SslProtocol <SslProtocol> works on <ActualProtocol>" -TestCases ($entry.Test) -Pending:($entry.Pending) { param($SslProtocol, $ActualProtocol) $params = @{ Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol SslProtocol = $SslProtocol SkipCertificateCheck = $true } $response = Invoke-WebRequest @params $result = $Response.Content | ConvertFrom-Json $result.headers.Host | Should -Be $params.Uri.Authority } } foreach ($entry in $testCases2) { It "Verifies Invoke-WebRequest -SslProtocol -SslProtocol <IntendedProtocol> fails on a <ActualProtocol> only connection" -TestCases ($entry.Test) -Pending:($entry.Pending) { param( $IntendedProtocol, $ActualProtocol) $params = @{ Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol SslProtocol = $IntendedProtocol SkipCertificateCheck = $true ErrorAction = 'Stop' } { Invoke-WebRequest @params } | Should -Throw -ErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } } } Context "Invoke-WebRequest File Resume Feature" { BeforeAll { $outFile = Join-Path $TestDrive "resume.txt" # Download the entire file to reference in tests $referenceFile = Join-Path $TestDrive "reference.txt" $resumeUri = Get-WebListenerUrl -Test 'Resume' Invoke-WebRequest -Uri $resumeUri -OutFile $referenceFile -ErrorAction Stop $referenceFileHash = Get-FileHash -Algorithm SHA256 -Path $referenceFile $referenceFileSize = Get-Item $referenceFile | Select-Object -ExpandProperty Length } AfterEach { Remove-Item -Force -ErrorAction 'SilentlyContinue' -Path $outFile } It "Invoke-WebRequest -Resume requires -OutFile" { { Invoke-WebRequest -Resume -Uri $resumeUri -ErrorAction Stop } | Should -Throw -ErrorId 'WebCmdletOutFileMissingException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } It "Invoke-WebRequest -Resume should fail if -OutFile folder" { { Invoke-WebRequest -Resume -Uri $resumeUri -OutFile $TestDrive -ErrorAction Stop } | Should -Throw -ErrorId 'WebCmdletResumeNotFilePathException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } It "Invoke-WebRequest -Resume Downloads the whole file when the file does not exist" { $response = Invoke-WebRequest -Uri $resumeUri -OutFile $outFile -Resume -PassThru $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $response.Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly 'bytes=0-' $response.StatusCode | Should -Be 206 $response.Headers.'Content-Range'[0] | Should -BeExactly "bytes 0-$($referenceFileSize-1)/$referenceFileSize" } It "Invoke-WebRequest -Resume overwrites an existing file that is larger than the remote file" { # Create a file larger than the download file $largerFileSize = $referenceFileSize + 20 1..$largerFileSize | ForEach-Object { [Byte]$_ } | Set-Content -AsByteStream $outFile $largerFileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $response = Invoke-WebRequest -Uri $resumeUri -OutFile $outFile -Resume -PassThru $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize Get-Item $outFile | Select-Object -ExpandProperty Length | Should -BeLessThan $largerFileSize $response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'false' $response.StatusCode | Should -Be 200 $response.Headers.ContainsKey('Content-Range') | Should -BeFalse } It "Invoke-WebRequest -Resume overwrites existing file when remote server does not support resume" { # Create a file larger than the download file $largerFileSize = $referenceFileSize + 20 1..$largerFileSize | ForEach-Object { [Byte]$_ } | Set-Content -AsByteStream $outFile $largerFileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'NoResume' $response = Invoke-WebRequest -Uri $uri -OutFile $outFile -Resume -PassThru $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $response.Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$largerFileSize-" $response.StatusCode | Should -Be 200 $response.Headers.ContainsKey('Content-Range') | Should -BeFalse } It "Invoke-WebRequest -Resume resumes downloading from <bytes> bytes" -TestCases @( @{bytes = 4} @{bytes = 8} @{bytes = 12} @{bytes = 16} ) { param($bytes, $statuscode) # Simulate partial download $uri = Get-WebListenerUrl -Test 'Resume' -TestValue "Bytes/$bytes" $null = Invoke-WebRequest -Uri $uri -OutFile $outFile Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $bytes $response = Invoke-WebRequest -Uri $resumeUri -OutFile $outFile -Resume -PassThru $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $response.Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$bytes-" $response.StatusCode | Should -Be 206 $response.Headers.'Content-Range'[0] | Should -BeExactly "bytes $bytes-$($referenceFileSize-1)/$referenceFileSize" } It "Invoke-WebRequest -Resume assumes the file was successfully completed when the local and remote file are the same size." { # Download the entire file $uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'NoResume' $null = Invoke-WebRequest -Uri $uri -OutFile $outFile $fileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $response = Invoke-WebRequest -Uri $resumeUri -OutFile $outFile -Resume -PassThru $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $response.Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$fileSize-" # The web cmdlets special case 416 as a success code when the local file and remote file are the same size $response.StatusCode | Should -Be 416 $response.Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize" } } Context "Invoke-WebRequest retry tests" { It "Invoke-WebRequest can retry - <Name>" -TestCases @( @{Name = "specified number of times - error 304"; failureCount = 2; failureCode = 304; retryCount = 2} @{Name = "specified number of times - error 400"; failureCount = 3; failureCode = 400; retryCount = 3} @{Name = "specified number of times - error 599"; failureCount = 1; failureCode = 599; retryCount = 2} @{Name = "specified number of times - error 404"; failureCount = 2; failureCode = 404; retryCount = 2} @{Name = "when retry count is higher than failure count"; failureCount = 2; failureCode = 404; retryCount = 4} ) { param($failureCount, $retryCount, $failureCode) $uri = Get-WebListenerUrl -Test 'Retry' -Query @{ sessionid = (New-Guid).Guid; failureCode = $failureCode; failureCount = $failureCount } $commandStr = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount $retryCount -RetryIntervalSec 1" $result = ExecuteWebCommand -command $commandStr $result.output.StatusCode | Should -Be "200" $jsonResult = $result.output.Content | ConvertFrom-Json $jsonResult.failureResponsesSent | Should -Be $failureCount } It "Invoke-WebRequest should fail when failureCount is greater than MaximumRetryCount" { $uri = Get-WebListenerUrl -Test 'Retry' -Query @{ sessionid = (New-Guid).Guid; failureCode = 400; failureCount = 4 } $command = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount 1 -RetryIntervalSec 1" $result = ExecuteWebCommand -command $command $jsonError = $result.error | ConvertFrom-Json $jsonError.error | Should -BeExactly 'Error: HTTP - 400 occurred.' } It "Invoke-WebRequest can retry with POST" { $uri = Get-WebListenerUrl -Test 'Retry' $sessionId = (New-Guid).Guid $body = @{ sessionid = $sessionId; failureCode = 404; failureCount = 1 } $commandStr = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body" $result = ExecuteWebCommand -command $commandStr $result.output.StatusCode | Should -Be "200" $jsonResult = $result.output.Content | ConvertFrom-Json $jsonResult.SessionId | Should -BeExactly $sessionId } It "Invoke-WebRequest respects the Retry-After header value in 429 status" { $Query = @{ statusCode = 429 reposnsephrase = 'Too Many Requests' contenttype = 'application/json' body = '{"message":"oops"}' headers = '{"Retry-After":"1"}' } $uri = Get-WebListenerUrl -Test 'Response' -Query $Query $verboseFile = Join-Path $TestDrive -ChildPath verbose.txt $result = Invoke-WebRequest -Uri $uri -MaximumRetryCount 1 -RetryIntervalSec 3 -SkipHttpErrorCheck -Verbose 4>$verbosefile $verboseFile | Should -FileContentMatch 'Retrying after interval of 1 seconds. Status code for previous attempt: TooManyRequests' } It "Invoke-WebRequest ignores the Retry-After header value NOT in 429 status" { $Query = @{ statusCode = 409 reposnsephrase = 'Conflict' contenttype = 'application/json' body = '{"message":"oops"}' headers = '{"Retry-After":"1"}' } $uri = Get-WebListenerUrl -Test 'Response' -Query $Query $verboseFile = Join-Path $TestDrive -ChildPath verbose.txt $result = Invoke-WebRequest -Uri $uri -MaximumRetryCount 1 -RetryIntervalSec 3 -SkipHttpErrorCheck -Verbose 4>$verbosefile $verboseFile | Should -FileContentMatch 'Retrying after interval of 3 seconds. Status code for previous attempt: Conflict' } } Context "Regex Parsing" { It 'correctly parses an image with id, class, and src attributes' { $dosUri = Get-WebListenerUrl -Test 'Dos' -query @{ dosType = 'img-attribute' } $response = Invoke-WebRequest -Uri $dosUri $response.Images | Should -Not -BeNullOrEmpty } $singleInputExpected = @( @{ Name = 'foo'; Value = 'bar' } ) It 'correctly parses input tag(s) for `<markup>`' -TestCases @( @{ Markup = "<input name='foo' value='bar'>"; ExpectedFields = $singleInputExpected }, @{ Markup = "<input name='foo' value='bar'/>"; ExpectedFields = $singleInputExpected }, @{ Markup = "<input name='foo' value='bar'>baz</input>"; ExpectedFields = $singleInputExpected } @{ Markup = "<input name='item1' value='bar'><input name='item2' value='foo'><input name='item3'></input><input name='item4' value='fu'><input name='item5' value='bahr'/>"; ExpectedFields = @( @{ Name = 'item1'; Value = 'bar'}, @{ Name = 'item2'; Value = 'foo' }, @{ Name = 'item3'; Value = $null }, @{ Name = 'item4'; Value = 'fu' }, @{ Name = 'item5'; Value = 'bahr' } ) } ) { param($markup, $expectedFields) $query = @{ contenttype = 'text/html' body = "<html><body>${markup}</body></html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $response = Invoke-WebRequest -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty ForEach ($expectedField in $expectedFields) { $actualField = $response.InputFields.FindByName($expectedField.Name) $actualField.Value | Should -Be $expectedField.Value } } } Context "Denial of service" -Tag 'DOS' { It "Image Parsing" { Set-ItResult -Pending -Because "The pathological regex runs fast due to https://github.com/dotnet/runtime/issues/33399. Fixed in .NET 5 preview.2" $dosUri = Get-WebListenerUrl -Test 'Dos' -query @{ dosType='img' dosLength='5000' } $script:content = '' [TimeSpan] $timeSpan = Measure-Command { $response = Invoke-WebRequest -Uri $dosUri $script:content = $response.content $response.Images | Out-Null } $script:content | Should -Not -BeNullOrEmpty # pathological regex $regex = [RegEx]::new('<img\s+[^>]*>') [TimeSpan] $pathologicalTimeSpan = Measure-Command { $regex.Match($content) } $pathologicalRatio = $pathologicalTimeSpan.TotalMilliseconds/$timeSpan.TotalMilliseconds Write-Verbose "Pathological ratio: $pathologicalRatio" -Verbose # dosLength 4,000 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 12 # dosLength 5,000 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 21 # dosLength 10,000 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 75 # in some cases we will be running in a Docker container with modest resources $pathologicalRatio | Should -BeGreaterThan 5 } It "Charset Parsing" { Set-ItResult -Pending -Because "We need a better way to test this and .NET has made bad regex's run faster" $dosUri = Get-WebListenerUrl -Test 'Dos' -query @{ dosType='charset' dosLength='2850' } $script:content = '' [TimeSpan] $timeSpan = Measure-Command { $response = Invoke-WebRequest -Uri $dosUri $script:content = $response.content } # Pathological regex $regex = [RegEx]::new('<meta\s[.\n]*[^><]*charset\s*=\s*["''\n]?(?<charset>[A-Za-z].[^\s"''\n<>]*)[\s"''\n>]') $script:content | Should -Not -BeNullOrEmpty [TimeSpan] $pathologicalTimeSpan = Measure-Command { $regex.Match($content) } $pathologicalRatio = $pathologicalTimeSpan.TotalMilliseconds/$timeSpan.TotalMilliseconds Write-Verbose "Pathological ratio: $pathologicalRatio" -Verbose # dosLength 2,750 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 13 # dosLength 2,850 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 22 # dosLength 3,000 on my 3.5 GHz 6-Core Intel Xeon E5 macpro produced a ratio of 31 # in some cases we will be running in a Docker container with modest resources $pathologicalRatio | Should -BeGreaterThan 5 } } Context 'Invoke-WebSession: Connection persistence in a WebSession' { # Match verbose message from resource name WebSessionConnectionRecreated with message: # The WebSession properties were changed between requests forcing all HTTP connections in the session to be recreated. $matchConnRecreatedMessage = [regex]::new('WebSession.+HTTP') function RunCheckingPersistence { param( [uri]$Uri, [string]$Command, [object]$Session, [switch]$ExpectConnectionRecreated, [switch]$CaptureSession ) $pwsh = [PowerShell]::Create() $pwsh.Runspace.SessionStateProxy.SetVariable('uri', $Uri) if ($Session) { $pwsh.Runspace.SessionStateProxy.SetVariable('Session', $Session) $command = "$command -WebSession `$Session" } if ($CaptureSession) { $command = "$command -SessionVariable Session" } $script = "`$null = $command -Verbose" $pwsh.AddScript($script).Invoke() $session = $pwsh.Runspace.SessionStateProxy.GetVariable('Session') $expectedConnRecreatedCount = if ($ExpectConnectionRecreated) { 1 } else { 0 } ($pwsh.Streams.Verbose | Where-Object { $matchConnRecreatedMessage.Matches($_.Message) }).Count | Should -Be $expectedConnRecreatedCount $pwsh.Dispose() return $session } It 'Connection persistence maintained' { $uri = Get-WebListenerUrl $Session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession 1 .. 3 | ForEach-Object { RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $Session } } It 'Connection persistence impacted by changing SkipCertificateCheck' { $uri = Get-WebListenerUrl -Https # This first request will throw because the certificate is invalid $session = $null $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession # No change in setting $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck:$false' -Session $session # Skipping cert check changes persistence $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck' -Session $session -ExpectConnectionRecreated # Same settings won't lose persistence $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck' -Session $session # Lose persistence due to changing cert check - this will also throw $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck:$false' -Session $session -ExpectConnectionRecreated } It 'Connection persistence is not impacted by changing request headers' { $uri = Get-WebListenerUrl $session = $null $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{ A = "B" }' -Session $session $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{}' -Session $session $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{ A = "C"; B = "D"}' -Session $session } It 'Connection persistence is impacted by changing the session cookie jar' { $uri = Get-WebListenerUrl $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession $session.Cookies = New-Object System.Net.CookieContainer $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session -ExpectConnectionRecreated # Adding a cookie to the container does not lose persistence $Session.Cookies.Add('http://localhost', [system.net.cookie]::new('cookie', 'value')) $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session } It 'Connection persistence is not impacted by changing the user agent' { $uri = Get-WebListenerUrl $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent Powershell' -CaptureSession $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent "PowerShell Core"' -Session $session $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent "PowerShell Core with HttpClient"' -Session $session # Ensure persistence is lost when we change a different setting $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy -UserAgent "PowerShell Core"' -Session $session -ExpectConnectionRecreated } It 'Connection persistence is not impacted when NoProxy parameter is not supplied' { $uri = Get-WebListenerUrl $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession # Explicitly prevent proxy - connection lost $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy' -Session $session -ExpectConnectionRecreated # Provide explicit switch value - connection maintained $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy:$true' -Session $session # No spec for proxy - connection maintained $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session # Two follow up calls without altering anything do not lose connection $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session } It 'Connection persistence is not impacted when SslProtocol parameter is not supplied' { $uri = Get-WebListenerUrl $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Tls12' -CaptureSession # No SslProtocol provided - keeps last value - connection retained $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session # Explicit default - loses connection $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Default' -Session $session -ExpectConnectionRecreated # No SslProtocol provided - keeps last value - connection retained $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session # Explicitly set to same value as last time it was set - connection retained $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Default' -Session $session } It 'Connection persistence is not impacted by Proxy and NoProxy unless changed parameters are used between invocations' { $uri = Get-WebListenerUrl $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession $proxy = 'http://127.0.0.1:8080' $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated # same proxy - do not lose persistence $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session # No proxy at all - use previous setting and don't lose connection $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session # NoProxy toggles proxy off - loses connection $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -NoProxy" -Session $session -ExpectConnectionRecreated # No setting at all - retains NoProxy setting $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session # Use proxy again - lose connection $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated # No proxy specified - connection retained $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session # Proxy changed - lose connection $proxy = 'http://localhost:8080' $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated # No proxy specified - connection retained $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session } } } Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener $NotFoundQuery = @{ statuscode = 404 responsephrase = 'Not Found' contenttype = 'application/json' body = '{"message": "oops"}' headers = "{}" } } AfterAll { $ProgressPreference = $oldProgress } #User-Agent changes on different platforms, so tests should only be run if on the correct platform It "Invoke-RestMethod returns Correct User-Agent on MacOSX" -Skip:(!$IsMacOS) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.'User-Agent' | Should -MatchExactly '.*\(Macintosh;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-RestMethod returns Correct User-Agent on Linux" -Skip:(!$IsLinux) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.'User-Agent' | Should -MatchExactly '.*\(Linux;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-RestMethod returns Correct User-Agent on Windows" -Skip:(!$IsWindows) { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.'User-Agent' | Should -MatchExactly '.*\(Windows NT \d+\.\d*;.*\) PowerShell\/\d+\.\d+\.\d+.*' } It "Invoke-RestMethod with blank ContentType succeeds" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -ContentType ''" $result = ExecuteWebCommand -command $command # Validate response $result.Error | Should -BeNullOrEmpty $result.Output.headers.'Content-Length' | Should -Be 0 } It "Invoke-RestMethod returns headers dictionary" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.Host | Should -Be $Uri.Authority } It "Validate Invoke-RestMethod -DisableKeepAlive" { # Operation options $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -DisableKeepAlive" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.Host | Should -Be $uri.Authority $result.Output.Headers.Connection | Should -Be "Close" } It "Validate Invoke-RestMethod -HttpVersion '<httpVersion>'" -Skip:(!$IsWindows) -TestCases @( @{ httpVersion = '1.1'}, @{ httpVersion = '2'} ) { param($httpVersion) if(Test-IsWinServer2012R2 -and $httpVersion -eq '2') { Set-ItResult -Skipped -Because "HTTP/2 is not supported on Windows Server 2012R2" return } # Operation options $uri = Get-WebListenerUrl -Test 'Get' -Https $command = "Invoke-RestMethod -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck" $result = ExecuteWebCommand -command $command # Validate response $result.Output.protocol | Should -Be "HTTP/$httpVersion" } It "Validate Invoke-RestMethod -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-RestMethod -Uri '$uri' -MaximumRedirection 4" $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.Host | Should -Match $uri.Authority } It "Validate Invoke-RestMethod error for -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-RestMethod -Uri '$uri' -MaximumRedirection 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Validate Invoke-RestMethod redirect with -Query destination Http" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{destination = $httpUri} $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Output.Headers.Host | Should -Be $httpUri.Authority } It "Validate Invoke-RestMethod redirect with -Query destination Https" { $httpsUri = Get-WebListenerUrl -Test 'Get' -Https $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpsUri} $command = "Invoke-RestMethod -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Output.Headers.Host | Should -Be $httpsUri.Authority } It "Invoke-RestMethod supports request that returns page containing UTF-8 data." { $uri = Get-WebListenerUrl -Test 'Encoding' -TestValue 'Utf8' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command $Result.Output | Should -Match '⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌' } It "Invoke-RestMethod -ContentType overwrites Content-Type from -Headers." { $uri = Get-WebListenerUrl -Test 'POST' $command = "Invoke-RestMethod -Uri '$uri' -ContentType 'application/json' -Headers @{'Content-Type'='plain/text'} -Method 'POST'" $result = ExecuteWebCommand -command $command $result.Output.headers."Content-Type" | Should -BeExactly 'application/json' } It "Invoke-RestMethod supports sending requests as UTF8" { $uri = Get-WebListenerUrl -Test POST # Body must contain non-ASCII characters $command = "Invoke-RestMethod -Uri '$uri' -body 'проверка' -ContentType 'application/json; charset=utf-8' -method 'POST'" $result = ExecuteWebCommand -command $command $Result.Output.Data | Should -BeExactly 'проверка' } It "Invoke-RestMethod supports sending XML requests without encoding" { $uri = Get-WebListenerUrl -Test POST $body = '<?xml version="1.0"?><foo />' $result = Invoke-RestMethod -Uri $uri -body ([xml]$body) -ContentType 'text/xml' -method 'POST' $result.Data | Should -BeExactly $body } It "Invoke-RestMethod supports request that returns page containing Code Page 936 data." { $uri = Get-WebListenerUrl -Test 'Encoding' -TestValue 'CP936' $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command $Result.Output | Should -Match '测试123' } It "Invoke-RestMethod validate ConnectionTimeoutSeconds option" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '5' $command = "Invoke-RestMethod -Uri '$uri' -ConnectionTimeoutSeconds 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "ConnectionTimeoutReached,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Invoke-RestMethod validate TimeoutSec alias" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '5' $command = "Invoke-RestMethod -Uri '$uri' -TimeoutSec 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "ConnectionTimeoutReached,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Validate Invoke-RestMethod error with -Proxy and -NoProxy option" { $uri = Get-WebListenerUrl -Test 'Delay' -TestValue '10' $command = "Invoke-RestMethod -Uri '$uri' -Proxy 'http://127.0.0.1:8080' -NoProxy -TimeoutSec 2" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "AmbiguousParameterSet,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } $testCase = @( @{ proxy_address = (Get-WebListenerUrl).Authority; name = 'HTTP proxy'; protocol = 'http' } @{ proxy_address = (Get-WebListenerUrl -https).Authority; name = 'HTTPS proxy'; protocol = 'https' } ) It "Validate Invoke-RestMethod with -Proxy option - '<name>'" -TestCases $testCase { param($proxy_address, $name, $protocol) # use external url, but with proxy the external url should not actually be called $command = "Invoke-RestMethod -Uri ${protocol}://httpbin.org -Proxy '${protocol}://${proxy_address}'" $result = ExecuteWebCommand -command $command $command = "Invoke-RestMethod -Uri '${protocol}://${proxy_address}' -NoProxy" $expectedResult = ExecuteWebCommand -command $command $result.Output | Should -BeExactly $expectedResult.Output } # Perform the following operation for Invoke-RestMethod # gzip Returns gzip-encoded data. # deflate Returns deflate-encoded data. # brotli Returns brotli-encoded data. # $dataEncodings = @("Chunked", "Compress", "Deflate", "GZip", "Identity") # Note: These are the supported options, but we do not have a web service to test them all. It "Invoke-RestMethod supports request that returns <DataEncoding>-encoded data." -TestCases @( @{ DataEncoding = "gzip" } @{ DataEncoding = "deflate" } @{ DataEncoding = "brotli" } ) { param($dataEncoding) $uri = Get-WebListenerUrl -Test 'Compression' -TestValue $dataEncoding $result = Invoke-RestMethod -Uri $uri # Validate response content # The content should be de-compressed. Otherwise, the above 'Invoke-RestMethod' would have thrown because converting to JSON internally would fail. $result.Headers.Host | Should -BeExactly $uri.Authority } # Perform the following operation for Invoke-RestMethod using the following content types: "text/plain", "application/xml", "application/xml" # post Returns POST data. # patch Returns PATCH data. # put Returns PUT data. # delete Returns DELETE data $testMethods = @("POST", "PATCH", "PUT", "DELETE") $contentTypes = @("text/plain", "application/xml", "application/json") foreach ($contentType in $contentTypes) { foreach ($method in $testMethods) { # Operation options $uri = Get-WebListenerUrl -Test $method $body = GetTestData -contentType $contentType $command = "Invoke-RestMethod -Uri $uri -Body '$body' -Method $method -ContentType $contentType" $commandNoContentType = "Invoke-RestMethod -Uri $uri -Body '$body' -Method $method" It "Invoke-RestMethod -Uri $uri -Method $method -ContentType $contentType -Body [body data]" { $result = ExecuteWebCommand -command $command # Validate response $result.Output.url | Should -Match $uri $result.Output.headers.'Content-Type' | Should -Match $contentType # Validate that the response Content.data field is the same as what we sent. $result.Output.data | Should -Be $body } It "Invoke-RestMethod -Uri $uri -Method $method -Body [body data]" { $result = ExecuteWebCommand -command $commandNoContentType # Validate response $result.Output.url | Should -Match $uri if ($method -eq "POST") { $result.Output.headers.'Content-Type' | Should -Match "application/x-www-form-urlencoded" # Validate that the response Content.form field is the same as what we sent. [string]$result.Output.form | Should -Be ([string][PSCustomObject]@{$body.Split("=")[0] = [System.Object[]]}) $result.Output.data | Should -BeNullOrEmpty } else { # Validate that the response Content.data field is the same as what we sent. $result.Output.data | Should -Be $body } } } } It "Validate Invoke-RestMethod -Headers --> Set KeepAlive to false via headers" { $uri = Get-WebListenerUrl -Test 'Get' $result = ExecuteRequestWithHeaders -cmdletName Invoke-RestMethod -uri $uri # Validate response $result.Output.url | Should -Match $uri $result.Output.Headers.Connection | Should -Be "Close" } # Validate all available user agents for Invoke-RestMethod $agents = @{ InternetExplorer = "MSIE 9.0" Chrome = "Chrome" Opera = "Opera" Safari = "Safari" FireFox = "Firefox" } foreach ($agentName in $agents.Keys) { $expectedAgent = $agents[$agentName] $uri = Get-WebListenerUrl -Test 'Get' $userAgent = "[Microsoft.PowerShell.Commands.PSUserAgent]::$agentName" $command = "Invoke-RestMethod -Uri $uri -UserAgent ($userAgent) " It "Validate Invoke-RestMethod UserAgent. Execute--> $command" { $result = ExecuteWebCommand -command $command # Validate response $result.Output.headers.Host | Should -Be $uri.Authority $result.Output.headers.'User-Agent' | Should -Match $expectedAgent } } It "Validate Invoke-RestMethod -OutFile" { $uri = Get-WebListenerUrl -Test 'Get' $result = ExecuteRequestWithOutFile -cmdletName "Invoke-RestMethod" -uri $uri $jsonContent = $result.Output | ConvertFrom-Json $jsonContent.headers.Host | Should -Be $uri.Authority } It "Invoke-RestMethod -OutFile folder Downloads the file and names it" { $uri = Get-WebListenerUrl -Test 'Get' $content = Invoke-WebRequest -Uri $uri $outFile = Join-Path $TestDrive $content.BaseResponse.RequestMessage.RequestUri.Segments[-1] # ensure the file does not exist Remove-Item -Force -ErrorAction Ignore -Path $outFile Invoke-RestMethod -Uri $uri -OutFile $TestDrive Test-Path $outFile | Should -Be $true Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $content.Content.Length } It "Invoke-RestMethod -PassThru -OutFile Downloads the file and pipes it" { $uri = Get-WebListenerUrl -Test 'Get' $content = Invoke-WebRequest -Uri $uri $outFile = Join-Path $TestDrive $content.BaseResponse.RequestMessage.RequestUri.Segments[-1] # ensure the file does not exist Remove-Item -Force -ErrorAction Ignore -Path $outFile $response = Invoke-RestMethod -Uri $uri -PassThru -OutFile $outFile # check if the file is downloaded. Test-Path $outFile | Should -Be $true # check if the file is correctly downloaded Get-Content -Path $outFile | Should -BeExactly $content.Content # check if the response stores the downloaded file contents # response is a PSCustomObject so converted it string for comparison $responseAsJsonString = $response | ConvertTo-Json -Compress $responseAsJsonString | Should -BeExactly $content.Content } It "Invoke-RestMethod should fail if -OutFile is <Name>." -TestCases @( @{ Name = "empty"; Value = [string]::Empty } @{ Name = "null"; Value = $null } ) { param ($value) $uri = Get-WebListenerUrl -Test 'Get' $errorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" { Invoke-RestMethod -Uri $uri -OutFile $value} | Should -Throw -ErrorId $errorId } It "Validate Invoke-RestMethod handles missing Content-Type in response header" { #Validate that exception is not thrown when response headers are missing Content-Type. $uri = Get-WebListenerUrl -Test 'ResponseHeaders' -Query @{'Content-Type' = ''} $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty } It "Validate Invoke-RestMethod StandardMethod and CustomMethod parameter sets" { $uri = Get-WebListenerUrl -Test 'Get' $errorId = "AmbiguousParameterSet,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" { Invoke-RestMethod -Uri $uri -Method GET -CustomMethod TEST } | Should -Throw -ErrorId $errorId } It "Validate CustomMethod method is used" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -CustomMethod TEST" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty $result.Output.method | Should -Be "TEST" } It "Validate Invoke-RestMethod default ContentType for CustomMethod POST" { $uri = Get-WebListenerUrl -Test 'Post' $command = "Invoke-RestMethod -Uri '$uri' -CustomMethod POST -Body 'testparam=testvalue'" $result = ExecuteWebCommand -command $command $result.Output.form.testparam | Should -Be "testvalue" $result.Output.Headers.'Content-Type' | Should -Be "application/x-www-form-urlencoded" } It "Validate Invoke-RestMethod body is converted to query params for CustomMethod GET" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -CustomMethod GET -Body @{'testparam'='testvalue'}" $result = ExecuteWebCommand -command $command $result.Output.args.testparam | Should -Be "testvalue" } It 'Validate Invoke-RestMethod empty body CustomMethod GET' { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -CustomMethod GET" $result = ExecuteWebCommand -command $command $result.Output.Headers.'Content-Length' | Should -BeNullOrEmpty } It "Validate Invoke-RestMethod body is converted to query params for CustomMethod GET and -NoProxy" { $uri = Get-WebListenerUrl -Test 'Get' $command = "Invoke-RestMethod -Uri '$uri' -CustomMethod GET -Body @{'testparam'='testvalue'} -NoProxy" $result = ExecuteWebCommand -command $command $result.Output.Query | Should -Be "?testparam=testvalue" } It "Validate Invoke-RestMethod returns HTTP errors in exception" { $query = @{ body = "I am a teapot!!!" statuscode = 418 responsephrase = "I am a teapot" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command $result.Error.ErrorDetails.Message | Should -Be $query.body $result.Error.Exception | Should -BeOfType Microsoft.PowerShell.Commands.HttpResponseException $result.Error.Exception.Response.StatusCode | Should -Be 418 $result.Error.Exception.Response.ReasonPhrase | Should -Be $query.responsephrase $result.Error.Exception.Message | Should -Match ": 418 \($($query.responsephrase)\)\." $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Validate Invoke-RestMethod -FollowRelLink doesn't fail if no Link Header is present" { $query = @{ contenttype = 'application/json' body = '"foo"' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command $result.Output | Should -BeExactly "foo" } It "Validate Invoke-RestMethod -FollowRelLink correctly follows all the available relation links <name>" -TestCases @( $maxLinks = 5 $originalUri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = $maxLinks} @{name = '(URI with scheme)'; uri = $originalUri} @{name = '(URI without scheme)'; uri = $originalUri.OriginalString.Split("//")[1]} ) { param($uri) $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command $result.Output.Count | Should -BeExactly $maxLinks 1..$maxLinks | ForEach-Object { $result.Output[$_ - 1].linknumber | Should -BeExactly $_ } } It "Validate Invoke-RestMethod -FollowRelLink correctly limits to -MaximumRelLink" { $maxLinks = 10 $maxLinksToFollow = 6 $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = $maxLinks} $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink -MaximumFollowRelLink $maxLinksToFollow" $result = ExecuteWebCommand -command $command $result.Output.Count | Should -BeExactly $maxLinksToFollow 1..$maxLinksToFollow | ForEach-Object { $result.Output[$_ - 1].linknumber | Should -BeExactly $_ } } It "Validate Invoke-RestMethod quietly ignores invalid Link Headers if -FollowRelLink is specified: <type>" -TestCases @( @{ type = "noUrl" } @{ type = "malformed" } @{ type = "noRel" } ) { param($type) $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command $result.Output.linknumber | Should -BeExactly 1 } It "Validate Invoke-RestMethod handles whitespace for Link Headers if -FollowRelLink is specified: <type>" -TestCases @( @{ type = "noWhitespace" } @{ type = "extraWhitespace" } ) { param($type) $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command 1..3 | ForEach-Object { $result.Output[$_ - 1].linknumber | Should -BeExactly $_ } } It "Validate Invoke-RestMethod -FollowRelLink correctly manages commas" { $maxLinks = 5 $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = $maxLinks; type = "with,comma"} $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command $result.Output.Count | Should -BeExactly $maxLinks 1..$maxLinks | ForEach-Object { $result.Output[$_ - 1].linknumber | Should -BeExactly $_ } } It "Verify Invoke-RestMethod supresses terminating errors with -SkipHttpErrorCheck" { $uri = Get-WebListenerUrl -Test 'Response' -Query $NotFoundQuery $command = "Invoke-RestMethod -SkipHttpErrorCheck -Uri '$uri'" $result = ExecuteWebCommand -Command $command $result.output.message | Should -BeExactly "oops" $result.output.error | Should -BeNullOrEmpty } It "Verify Invoke-RestMethod terminates without -SkipHttpErrorCheck" { $uri = Get-WebListenerUrl -Test 'Response' -Query $NotFoundQuery $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -Command $command $result.output | Should -BeNullOrEmpty $result.error | Should -Not -BeNullOrEmpty } It "Verify Invoke-RestMethod assigns 200 status code with -StatusCodeVariable" { $query = @{ statuscode = 200 responsephrase = 'OK' contenttype = 'application/json' body = '{"message": "works"}' headers = "{}" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -StatusCodeVariable code -Uri "$uri" $code | Should -Be 200 } It "Verify Invoke-RestMethod assigns 404 status code with -StatusCodeVariable" { $query = @{ statuscode = 404 responsephrase = 'Not Found' contenttype = 'application/json' body = '{"message": "oops"}' headers = "{}" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -SkipHttpErrorCheck -StatusCodeVariable code -Uri "$uri" $code | Should -Be 404 } It "Verify Invoke-RestMethod assigns 500 status code with -StatusCodeVariable" { $query = @{ statuscode = 500 responsephrase = 'Internal Server Error' contenttype = 'application/json' body = '{"message": "oops"}' headers = "{}" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -SkipHttpErrorCheck -StatusCodeVariable code -Uri "$uri" $code | Should -Be 500 } #region Redirect tests It "Validates Invoke-RestMethod with -PreserveAuthorizationOnRedirect preserves the authorization header on redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri $uri -PreserveAuthorizationOnRedirect $response.Error | Should -BeNullOrEmpty # ensure Authorization header has been preserved. $response.Content.Headers."Authorization" | Should -BeExactly "test" } It "Validates Invoke-RestMethod preserves the authorization header on multiple redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue 3 -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri $uri -PreserveAuthorizationOnRedirect $response.Error | Should -BeNullOrEmpty # ensure Authorization header was stripped $response.Content.Headers."Authorization" | Should -BeExactly "test" } It "Validates Invoke-RestMethod with -WebSession and -PreserveAuthorizationOnRedirect doesn't change session variable on multiple redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) #[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")] $token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force $credential = [pscredential]::new("testuser", $token) $certificate = Get-WebListenerClientCertificate $headers = @{"Authorization" = "test"} $proxy = (Get-WebListenerUrl).Authority $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue 2 -Query @{type = $redirectType} $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.MaximumRedirection = 2 $session.MaximumRetryCount = 2 $session.RetryIntervalInSeconds = 2 $session.UseDefaultCredentials = $true $null = Invoke-RestMethod -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -AllowUnencryptedAuthentication -Headers $headers $session.MaximumRedirection | Should -BeExactly 2 $session.MaximumRetryCount | Should -BeExactly 2 $session.RetryIntervalInSeconds | Should -BeExactly 2 $session.UseDefaultCredentials | Should -BeExactly $true $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.Credentials = $credential $session.Certificates = [System.Security.Cryptography.X509Certificates.X509CertificateCollection]::new([X509Certificate]$certificate) $null = Invoke-RestMethod -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -SkipCertificateCheck -Headers $headers $session.Credentials.UserName | Should -BeExactly $credential.UserName $session.Credentials.Password | Should -BeExactly $credential.GetNetworkCredential().Password $session.Certificates.Thumbprint | Should -BeExactly $certificate.Thumbprint $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $session.Proxy = [System.Net.WebProxy]::new($proxy) $null = Invoke-RestMethod -Uri $uri -PreserveAuthorizationOnRedirect -WebSession $session -Headers $headers $session.Proxy.GetProxy($uri).Authority | Should -BeExactly $proxy } It "Validates Invoke-RestMethod strips the authorization header on various redirects: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri $uri $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been removed. $response.Content.Headers."Authorization" | Should -BeNullOrEmpty } # NOTE: Only testing redirection of POST -> GET for unique underlying values of HttpStatusCode. # Some names overlap in underlying value. It "Validates Invoke-RestMethod strips the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been removed. $response.Content.Headers."Authorization" | Should -BeNullOrEmpty # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-RestMethod -PreserveAuthorizationOnRedirect keeps the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been preserved. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-RestMethod -PreserveAuthorizationOnRedirect -CustomMethod POST keeps the authorization header redirects and switches from POST to GET when it handles the redirect: <redirectType> <redirectedMethod>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -CustomMethod 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been preserved. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } It "Validates Invoke-RestMethod -PreserveHttpMethodOnRedirect keeps the authorization header redirects and remains POST when it handles the redirect: <redirectType>" -TestCases $redirectTests { param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty # ensure Authorization header has been kept. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST doesn't change. $response.Content.Method | Should -Be 'POST' } It "Validates Invoke-RestMethod handles responses without Location header for requests with Authorization header and redirect: <redirectType>" -TestCases $redirectTests { param($redirectType, $redirectedMethod) # Skip relative test as it is not a valid response type. if ($redirectType -eq 'relative') { return } # When an Authorization request header is present, # and -PreserveAuthorizationOnRedirect is not present, # PowerShell should throw an HTTP Response Exception # for a redirect response which does not contain a Location response header. # The correct redirect status code should be included in the exception. $StatusCode = [int][System.Net.HttpStatusCode]$redirectType $uri = Get-WebListenerUrl -Test Response -Query @{statuscode = $StatusCode} $command = "Invoke-RestMethod -Uri '$uri' -Headers @{Authorization = 'foo'}" $response = ExecuteWebCommand -command $command $response.Error.Exception | Should -BeOfType Microsoft.PowerShell.Commands.HttpResponseException $response.Error.Exception.Response.StatusCode | Should -Be $StatusCode $response.Error.Exception.Response.Headers.Location | Should -BeNullOrEmpty } It "Validate Invoke-RestMethod Https to Http redirect with -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri} $command = "Invoke-RestMethod -Uri '$uri' -SkipCertificateCheck -AllowInsecureRedirect" $result = ExecuteWebCommand -command $command $result.Output.Headers.Host | Should -Be $httpUri.Authority } It "Validate Invoke-RestMethod Https to Http redirect without -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri} $command = "Invoke-RestMethod -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "InsecureRedirection,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Validate Invoke-RestMethod Https to Http (No Scheme) redirect without -AllowInsecureRedirect" { $httpUri = Get-WebListenerUrl -Test 'Get' $uri = Get-WebListenerUrl -Test 'Redirect' -Https -Query @{destination = $httpUri.Authority} $command = "Invoke-RestMethod -Uri '$uri' -SkipCertificateCheck" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } #endregion Redirect tests Context "Invoke-RestMethod SkipHeaderVerification Tests" { BeforeAll { $Testfile = Join-Path $testdrive 'SkipHeaderVerification.txt' 'bar' | Set-Content $Testfile } It "Verifies Invoke-RestMethod default header handling with no errors" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "*"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers -Cmdlet "Invoke-RestMethod" $response.Error | Should -BeNullOrEmpty $response.Content.Headers."If-Match" | Should -BeExactly "*" } It "Verifies Invoke-RestMethod default header handling reports an error is returned for an invalid If-Match header value" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "12345"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers -Cmdlet "Invoke-RestMethod" $response.Error | Should -Not -BeNullOrEmpty $response.Error.FullyQualifiedErrorId | Should -Be "System.FormatException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" $response.Error.Exception.Message | Should -Be "The format of value '12345' is invalid." } It "Verifies Invoke-RestMethod header handling does not report an error when using -SkipHeaderValidation" { $uri = Get-WebListenerUrl -Test 'Get' $headers = @{"If-Match" = "12345"} $response = ExecuteRequestWithCustomHeaders -Uri $uri -headers $headers -SkipHeaderValidation -Cmdlet "Invoke-RestMethod" $response.Error | Should -BeNullOrEmpty $response.Content.Headers."If-Match" | Should -BeExactly "12345" } It "Verifies Invoke-RestMethod default UserAgent handling with no errors" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -Cmdlet "Invoke-RestMethod" $response.Error | Should -BeNullOrEmpty $Pattern = [regex]::Escape($UserAgent) $response.Content.Headers."User-Agent" | Should -Match $Pattern } It "Verifies Invoke-RestMethod default UserAgent handling reports an error is returned for an invalid UserAgent value" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = 'Invalid:Agent' $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -Cmdlet "Invoke-RestMethod" $response.Error | Should -Not -BeNullOrEmpty $response.Error.FullyQualifiedErrorId | Should -Be "System.FormatException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" $response.Error.Exception.Message | Should -Be "The format of value 'Invalid:Agent' is invalid." } It "Verifies Invoke-RestMethod UserAgent handling does not report an error when using -SkipHeaderValidation" { $uri = Get-WebListenerUrl -Test 'Get' $UserAgent = 'Invalid:Agent' $response = ExecuteRequestWithCustomUserAgent -Uri $uri -UserAgent $UserAgent -SkipHeaderValidation -Cmdlet "Invoke-RestMethod" $response.Error | Should -BeNullOrEmpty $Pattern = [regex]::Escape($UserAgent) $response.Content.Headers."User-Agent" | Should -Match $Pattern } It "Verifies Invoke-RestMethod default ContentType handling reports no error is returned for a valid Content-Type header value and -Body" { $contentType = 'text/plain' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' $result = Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -Body $body $result.data | Should -BeExactly $body $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-RestMethod default ContentType handling reports an error is returned for an invalid Content-Type header value and -Body" { $contentType = 'foo' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' { Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -Body $body -ErrorAction 'Stop' } | Should -Throw -ErrorId "WebCmdletContentTypeException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod ContentType handling reports no error is returned for an invalid Content-Type header value, -Body, and -SkipHeaderValidation" { $contentType = 'foo' $body = "bar" $uri = Get-WebListenerUrl -Test 'Post' $result = Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -Body $body -SkipHeaderValidation $result.data | Should -BeExactly $body $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-RestMethod default ContentType handling reports no error is returned for a valid Content-Type header value and -InFile" { $contentType = 'text/plain' $uri = Get-WebListenerUrl -Test 'Post' $result = Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile # Match used due to inconsistent newline rendering $result.data | Should -Match 'bar' $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-RestMethod default ContentType handling reports an error is returned for an invalid Content-Type header value and -InFile" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Post' { Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile -ErrorAction 'Stop' } | Should -Throw -ErrorId "WebCmdletContentTypeException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod default ContentType handling reports no error is returned for an invalid Content-Type header value, -Infile, and -SkipHeaderValidation" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Post' $result = Invoke-RestMethod -Uri $uri -Method 'Post' -ContentType $contentType -InFile $Testfile -SkipHeaderValidation # Match used due to inconsistent newline rendering $result.data | Should -Match 'bar' $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-RestMethod applies -ContentType when no -Body is present" { $contentType = 'application/json' $uri = Get-WebListenerUrl -Test 'Get' $result = Invoke-RestMethod -Uri $uri -Method 'GET' -ContentType $contentType $result.data | Should -BeNullOrEmpty $result.headers.'Content-Type' | Should -BeExactly $contentType } It "Verifies Invoke-RestMethod applies an invalid -ContentType when no -Body is present and -SkipHeaderValidation is present" { $contentType = 'foo' $uri = Get-WebListenerUrl -Test 'Get' $result = Invoke-RestMethod -Uri $uri -Method 'GET' -ContentType $contentType -SkipHeaderValidation $result.data | Should -BeNullOrEmpty $result.headers.'Content-Type' | Should -BeExactly $contentType } } Context "HTTPS Tests" { It "Validate Invoke-RestMethod -SkipCertificateCheck" { # HTTP method HEAD must be used to not retrieve an unparsable HTTP body # validate that exception is thrown for URI with expired certificate $uri = Get-WebListenerUrl -Https $command = "Invoke-RestMethod -Uri '$uri' -Method HEAD" $result = ExecuteWebCommand -command $command $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" # validate that no exception is thrown for URI with expired certificate when using -SkipCertificateCheck option $command = "Invoke-RestMethod -Uri '$uri' -SkipCertificateCheck -Method HEAD" $result = ExecuteWebCommand -command $command $result.Error | Should -BeNullOrEmpty } It "Validate Invoke-RestMethod returns native HTTPS error message in exception" { $uri = Get-WebListenerUrl -Https $command = "Invoke-RestMethod -Uri '$uri'" $result = ExecuteWebCommand -command $command # need to check against inner exception since Linux and Windows uses different HTTP client libraries so errors aren't the same $result.Error.ErrorDetails.Message | Should -Match $result.Error.Exception.InnerException.Message $result.Error.FullyQualifiedErrorId | Should -Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod Certificate Authentication Fails without -Certificate" { $uri = Get-WebListenerUrl -Https -Test 'Cert' $result = Invoke-RestMethod -Uri $uri -SkipCertificateCheck $result.Status | Should -Be 'FAILED' } It "Verifies Invoke-RestMethod Certificate Authentication Successful with -Certificate" { $uri = Get-WebListenerUrl -Https -Test 'Cert' $certificate = Get-WebListenerClientCertificate $result = Invoke-RestMethod -Uri $uri -Certificate $certificate -SkipCertificateCheck $result.Status | Should -Be 'OK' $result.Thumbprint | Should -Be $certificate.Thumbprint } } Context "Multipart/form-data Tests" { <# Content-Type request headers for multipart/form-data appear as: multipart/form-data; boundary="0ab0cb90-f01b-4c15-bd4d-53d073efcc1d" MultipartFormDataContent sets a random GUID for the boundary before submitting the request to the remote endpoint. Tests in this context for Content-Type match 'multipart/form-data' as we do not have access to the random GUID. #> <# Kestrel/ASP.NET inconsistently renders the new line for uploaded text files. File content tests in this context use match as a workaround. #> BeforeAll { $file1Name = "testfile1.txt" $file1Path = Join-Path $testdrive $file1Name $file1Contents = "Test123" $file1Contents | Set-Content $file1Path -Force $file2Name = "testfile2.txt" $file2Path = Join-Path $testdrive $file2Name $file2Contents = "Test456" $file2Contents | Set-Content $file2Path -Force $file3Name = "Kündigung_Mustermann_Max.TTA_2023_01_30.txt" $file3Path = Join-Path $testdrive $file3Name $file3Contents = "Test789" $file3Contents | Set-Content $file3Path -Force } It "Verifies Invoke-RestMethod Supports Multipart String Values" { $body = GetMultipartBody -String $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString[0] | Should -Be 'TestValue' } It "Verifies Invoke-RestMethod Supports Multipart File Values" { $body = GetMultipartBody -File $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files[0].FileName | Should -Be 'multipart.txt' $result.Files[0].ContentType | Should -Be 'text/plain' $result.Files[0].Content | Should -Match 'TestContent' } It "Verifies Invoke-RestMethod Supports Mixed Multipart String and File Values" { $body = GetMultipartBody -String -File $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString[0] | Should -Be 'TestValue' $result.Files[0].FileName | Should -Be 'multipart.txt' $result.Files[0].ContentType | Should -Be 'text/plain' $result.Files[0].Content | Should -Match 'TestContent' } It "Verifies Invoke-RestMethod -Form supports string values" { $form = @{TestString = "TestValue"} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestString.Count | Should -Be 1 $result.Items.TestString[0] | Should -Be 'TestValue' } It "Verifies Invoke-RestMethod -Form supports a collection of string values" { $form = @{TestStrings = "TestValue", "TestValue2"} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestStrings.Count | Should -Be 2 $result.Items.TestStrings[0] | Should -Be 'TestValue' $result.Items.TestStrings[1] | Should -Be 'TestValue2' } It "Verifies Invoke-RestMethod -Form supports file values" { $form = @{TestFile = [System.IO.FileInfo]$file1Path} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].Name | Should -Be "TestFile" $result.Files[0].FileName | Should -Be $file1Name $result.Files[0].ContentType | Should -Be 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents } It "Verifies Invoke-RestMethod -Form supports read-only file values" { # Create a read-only test file $readOnlyFile = Join-Path $TestDrive "readonly-test.txt" "ReadOnly test content" | Out-File -FilePath $readOnlyFile -Encoding utf8 Set-ItemProperty -Path $readOnlyFile -Name IsReadOnly -Value $true $form = @{TestFile = [System.IO.FileInfo]$readOnlyFile} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].Name | Should -Be "TestFile" $result.Files[0].FileName | Should -Be "readonly-test.txt" $result.Files[0].ContentType | Should -Be 'application/octet-stream' $result.Files[0].Content | Should -Match "ReadOnly test content" } It "Verifies Invoke-RestMethod -Form sets Content-Disposition FileName and FileNameStar." { $ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("attachment") $ContentDisposition.FileName = $fileName $ContentDisposition.FileNameStar = $fileName $form = @{TestFile = [System.IO.FileInfo]$file3Path} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 1 $result.Files[0].ContentDisposition.FileName | Should -Be $ContentDisposition.FileName $result.Files[0].ContentDisposition.FileNameStar | Should -Be $ContentDisposition.FileNameStar } It "Verifies Invoke-RestMethod -Form supports a collection of file values" { $form = @{TestFiles = [System.IO.FileInfo]$file1Path, [System.IO.FileInfo]$file2Path} $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Files.Count | Should -Be 2 $result.Files[0].Name | Should -Be "TestFiles" $result.Files[0].FileName | Should -Be $file1Name $result.Files[0].ContentType | Should -Be 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents $result.Files[1].Name | Should -Be "TestFiles" $result.Files[1].FileName | Should -Be $file2Name $result.Files[1].ContentType | Should -Be 'application/octet-stream' $result.Files[1].Content | Should -Match $file2Contents } It "Verifies Invoke-RestMethod -Form supports combinations of strings and files" { $form = @{ TestStrings = "TestValue", "TestValue2" TestFiles = [System.IO.FileInfo]$file1Path, [System.IO.FileInfo]$file2Path } $uri = Get-WebListenerUrl -Test 'Multipart' $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' $result.Items.TestStrings.Count | Should -Be 2 $result.Files.Count | Should -Be 2 $result.Items.TestStrings[0] | Should -Be 'TestValue' $result.Items.TestStrings[1] | Should -Be 'TestValue2' $result.Files[0].Name | Should -Be "TestFiles" $result.Files[0].FileName | Should -Be $file1Name $result.Files[0].ContentType | Should -Be 'application/octet-stream' $result.Files[0].Content | Should -Match $file1Contents $result.Files[1].Name | Should -Be "TestFiles" $result.Files[1].FileName | Should -Be $file2Name $result.Files[1].ContentType | Should -Be 'application/octet-stream' $result.Files[1].Content | Should -Match $file2Contents } It "Verifies Invoke-RestMethod -Form is mutually exclusive with -Body" { $form = @{TestString = "TestValue"} $body = "test" $uri = Get-WebListenerUrl -Test 'Multipart' {Invoke-RestMethod -Uri $uri -Form $form -Body $Body -ErrorAction 'Stop'} | Should -Throw -ErrorId 'WebCmdletBodyFormConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } It "Verifies Invoke-RestMethod -Form is mutually exclusive with -InFile" { $form = @{TestString = "TestValue"} $uri = Get-WebListenerUrl -Test 'Multipart' {Invoke-RestMethod -Uri $uri -Form $form -InFile $file1Path -ErrorAction 'Stop'} | Should -Throw -ErrorId 'WebCmdletFormInFileConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } } It "Verifies Invoke-RestMethod does not sent expect 100-continue headers by default" { $uri = Get-WebListenerUrl -Test 'Get' $result = Invoke-RestMethod -Uri $uri $result.headers.Expect | Should -BeNullOrEmpty $result.method | Should -BeExactly "GET" $result.url | Should -BeExactly $uri.ToString() } It "Verifies Invoke-RestMethod sends expect 100-continue header when defined in -Headers" { $uri = Get-WebListenerUrl -Test 'Get' $result = Invoke-RestMethod -Uri $uri -Headers @{Expect = '100-continue'} $result.headers.Expect | Should -BeExactly '100-continue' $result.method | Should -BeExactly "GET" $result.url | Should -BeExactly $uri.ToString() } #region charset encoding tests Context "Invoke-RestMethod Encoding tests with BasicHtmlWebResponseObject response" { It "Verifies Invoke-RestMethod detects charset meta value when the ContentType header does not define it." { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset="Unicode"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-WebRequest detects charset meta value when newlines are encountered in the element." { $query = @{ contenttype = 'text/html' body = "<html>`n <head>`n <meta`n charset=`"Unicode`"`n >`n </head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod detects charset meta value when the attribute value is unquoted." { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset = Unicode></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod detects http-equiv charset meta value when the ContentType header does not define it." { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta http-equiv=`"content-type`" content=`"text/html; charset=Unicode`">`n</head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod detects http-equiv charset meta value newlines are encountered in the element." { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta`n http-equiv=`"content-type`"`n content=`"text/html; charset=Unicode`">`n</head>`n</html>`n" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::GetEncoding('Unicode') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod ignores meta charset value when Content-Type header defines it." { $query = @{ contenttype = 'text/html; charset=utf-8' body = '<html><head><meta charset="utf-32"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query # NOTE: meta charset should be ignored $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod honors non-utf8 charsets in the Content-Type header" { $query = @{ contenttype = 'text/html; charset=utf-16' body = '<html><head><meta charset="utf-32"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query # NOTE: meta charset should be ignored $expectedEncoding = [System.Text.Encoding]::GetEncoding('utf-16') $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod defaults to UTF-8 when an unsupported/invalid charset is declared" { $query = @{ contenttype = 'text/html' body = '<html><head><meta charset="invalid"></head></html>' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } It "Verifies Invoke-RestMethod defaults to UTF-8 when an unsupported/invalid charset is declared using http-equiv" { $query = @{ contenttype = 'text/html' body = "<html><head>`n<meta http-equiv=`"content-type`" content=`"text/html; charset=Invalid`">`n</head>`n</html>" } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $expectedEncoding = [System.Text.Encoding]::UTF8 $response = ExecuteRestMethod -Uri $uri -UseBasicParsing $response.Error | Should -BeNullOrEmpty $response.Encoding.EncodingName | Should -Be $expectedEncoding.EncodingName } } #endregion charset encoding tests Context 'Invoke-RestMethod ResponseHeadersVariable Tests' { It "Verifies Invoke-RestMethod supports -ResponseHeadersVariable" { $uri = Get-WebListenerUrl -Test '/' $response = Invoke-RestMethod -Uri $uri -ResponseHeadersVariable 'headers' $headers.'Content-Type' | Should -Be 'text/html; charset=utf-8' $headers.Server | Should -Be 'Kestrel' } It "Verifies Invoke-RestMethod supports -ResponseHeadersVariable overwriting existing variable" { $uri = Get-WebListenerUrl -Test '/' $headers = 'prexisting' $response = Invoke-RestMethod -Uri $uri -ResponseHeadersVariable 'headers' $headers | Should -Not -Be 'prexisting' $headers.'Content-Type' | Should -Be 'text/html; charset=utf-8' $headers.Server | Should -Be 'Kestrel' } } Context "Invoke-RestMethod -Authentication tests" { BeforeAll { #[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")] $token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force $credential = [pscredential]::new("testuser", $token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} ) } It "Verifies Invoke-RestMethod -Authentication Basic" { $params = @{ Uri = $httpsUri Authentication = "Basic" Credential = $credential SkipCertificateCheck = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } It "Verifies Invoke-RestMethod -Authentication Basic with null username" { $credential = [pscredential]::new([PSCustomObject]@{UserName = $null;Password=$token.psobject.BaseObject}) $params = @{ Uri = $httpsUri Authentication = "Basic" Credential = $credential SkipCertificateCheck = $true } $Response = Invoke-RestMethod @params $Response.Headers.Authorization | Should -BeExactly "Basic OnRlc3RwYXNzd29yZA==" } It "Verifies Invoke-RestMethod -Authentication <Authentication>" -TestCases $testCases { param($Authentication) $params = @{ Uri = $httpsUri Authentication = $Authentication Token = $token SkipCertificateCheck = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -BeExactly "Bearer testpassword" } It "Verifies Invoke-RestMethod -Authentication does not support -UseDefaultCredentials" { $params = @{ Uri = $httpsUri Token = $token Authentication = "OAuth" UseDefaultCredentials = $true ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAuthenticationConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod -Authentication does not support Both -Credential and -Token" { $params = @{ Uri = $httpsUri Token = $token Credential = $credential Authentication = "OAuth" ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAuthenticationTokenConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod -Authentication <Authentication> requires -Token" -TestCases $testCases { param($Authentication) $params = @{ Uri = $httpsUri Authentication = $Authentication ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAuthenticationTokenNotSuppliedException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod -Authentication Basic requires -Credential" { $params = @{ Uri = $httpsUri Authentication = "Basic" ErrorAction = 'Stop' SkipCertificateCheck = $true } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAuthenticationCredentialNotSuppliedException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod -Authentication Requires HTTPS" { $params = @{ Uri = $httpUri Token = $token Authentication = "OAuth" ErrorAction = 'Stop' } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod -Authentication Can use HTTP with -AllowUnencryptedAuthentication" { $params = @{ Uri = $httpUri Token = $token Authentication = "OAuth" AllowUnencryptedAuthentication = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -BeExactly "Bearer testpassword" } It "Verifies Invoke-RestMethod Negotiated -Credential over HTTPS" { $params = @{ Uri = $httpsBasicUri Credential = $credential SkipCertificateCheck = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } It "Verifies Invoke-RestMethod Negotiated -Credential Requires HTTPS" { $params = @{ Uri = $httpBasicUri Credential = $credential ErrorAction = 'Stop' } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } It "Verifies Invoke-RestMethod Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { $params = @{ Uri = $httpBasicUri Credential = $credential AllowUnencryptedAuthentication = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } # UseDefaultCredentials is only reliably testable on Windows It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials with '<AuthType>' over HTTPS" -Skip:$(!$IsWindows) -TestCases @( @{AuthType = 'NTLM'} @{AuthType = 'Negotiate'} ) { param($AuthType) $params = @{ Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType -Https UseDefaultCredentials = $true SkipCertificateCheck = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -Match "^$AuthType " } # The error condition can at least be tested on all platforms. It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials Requires HTTPS" { $params = @{ Uri = $httpUri UseDefaultCredentials = $true ErrorAction = 'Stop' } { Invoke-RestMethod @params } | Should -Throw -ErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" } # UseDefaultCredentials is only reliably testable on Windows It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials with '<AuthType>' Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) -TestCases @( @{AuthType = 'NTLM'} @{AuthType = 'Negotiate'} ) { param($AuthType) $params = @{ Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType UseDefaultCredentials = $true AllowUnencryptedAuthentication = $true } $result = Invoke-RestMethod @params $result.Headers.Authorization | Should -Match "^$AuthType " } } Context "Invoke-RestMethod -SslProtocol Test" { BeforeAll { # We put Tls13 tests at pending due to modern OS limitations. # Tracking issue https://github.com/PowerShell/PowerShell/issues/13439 $skipForTls1 = $true $testCases1 = @( @{ Test = @{SslProtocol = 'Default'; ActualProtocol = 'Default'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls11'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls13'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12, Tls13'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls12'}; Pending = $false } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12, Tls13'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls11'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } @{ Test = @{SslProtocol = 'Tls, Tls11, Tls13'; ActualProtocol = 'Tls'}; Pending = $true } @{ Test = @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls'}; Pending = $skipForTls1 } # Skipping intermediary protocols is not supported on all platforms # Removed this as Tls now default to Tls12 # @{ Test = @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls'}; Pending = -not $IsWindows } @{ Test = @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls12'}; Pending = -not $IsWindows } ) $testCases2 = @( @{ Test = @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls13'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls13'; ActualProtocol = 'Tls'}; Pending = $true } @{ Test = @{IntendedProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls13'}; Pending = $true } ) } foreach ($entry in $testCases1) { It "Verifies Invoke-RestMethod -SslProtocol <SslProtocol> works on <ActualProtocol>" -TestCases ($entry.Test) -Pending:($entry.Pending) { param($SslProtocol, $ActualProtocol) $params = @{ Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol SslProtocol = $SslProtocol SkipCertificateCheck = $true } $result = Invoke-RestMethod @params $result.headers.Host | Should -Be $params.Uri.Authority } } foreach ($entry in $testCases2) { It "Verifies Invoke-RestMethod -SslProtocol <IntendedProtocol> fails on a <ActualProtocol> only connection" -TestCases ($entry.Test) -Pending:($entry.Pending) { param( $IntendedProtocol, $ActualProtocol) $params = @{ Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol SslProtocol = $IntendedProtocol SkipCertificateCheck = $true ErrorAction = 'Stop' } { Invoke-RestMethod @params } | Should -Throw -ErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } } } Context "Invoke-RestMethod Single Value JSON null support" { It "Invoke-RestMethod Supports a Single Value JSON null" { $query = @{ contenttype = 'application/json' body = 'null' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -Uri $uri | Should -Be $null } It "Invoke-RestMethod Supports a Single Value JSON null and ignores whitespace" { $query = @{ contenttype = 'application/json' body = " null " } $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -Uri $uri | Should -Be $null $query['body'] = " null `n" $uri = Get-WebListenerUrl -Test 'Response' -Query $query Invoke-RestMethod -Uri $uri | Should -Be $null } } Context "Invoke-RestMethod File Resume Feature" { BeforeAll { $outFile = Join-Path $TestDrive "resume.txt" # Download the entire file to reference in tests $referenceFile = Join-Path $TestDrive "reference.txt" $resumeUri = Get-WebListenerUrl -Test 'Resume' Invoke-RestMethod -Uri $resumeUri -OutFile $referenceFile -ErrorAction Stop $referenceFileHash = Get-FileHash -Algorithm SHA256 -Path $referenceFile $referenceFileSize = Get-Item $referenceFile | Select-Object -ExpandProperty Length } AfterEach { Remove-Item -Force -ErrorAction 'SilentlyContinue' -Path $outFile } It "Invoke-RestMethod -Resume requires -OutFile" { { Invoke-RestMethod -Resume -Uri $resumeUri -ErrorAction Stop } | Should -Throw -ErrorId 'WebCmdletOutFileMissingException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } It "Invoke-RestMethod -Resume should fail if -OutFile folder" { { Invoke-RestMethod -Resume -Uri $resumeUri -OutFile $TestDrive -ErrorAction Stop } | Should -Throw -ErrorId 'WebCmdletResumeNotFilePathException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } It "Invoke-RestMethod -Resume Downloads the whole file when the file does not exist" { # ensure the file does not exist Remove-Item -Force -ErrorAction 'SilentlyContinue' -Path $outFile Invoke-RestMethod -Uri $resumeUri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly 'bytes=0-' $Headers.'Content-Range'[0] | Should -BeExactly "bytes 0-$($referenceFileSize-1)/$referenceFileSize" } It "Invoke-RestMethod -Resume overwrites an existing file that is larger than the remote file" { # Create a file larger than the download file $largerFileSize = $referenceFileSize + 20 1..$largerFileSize | ForEach-Object { [Byte]$_ } | Set-Content -AsByteStream $outFile $largerFileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $response = Invoke-RestMethod -Uri $resumeUri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize Get-Item $outFile | Select-Object -ExpandProperty Length | Should -BeLessThan $largerFileSize $Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'false' $Headers.ContainsKey('Content-Range') | Should -BeFalse } It "Invoke-RestMethod -Resume overwrites existing file when remote server does not support resume" { # Create a file larger than the download file $largerFileSize = $referenceFileSize + 20 1..$largerFileSize | ForEach-Object { [Byte]$_ } | Set-Content -AsByteStream $outFile $largerFileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'NoResume' $response = Invoke-RestMethod -Uri $uri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize Get-Item $outFile | Select-Object -ExpandProperty Length | Should -BeLessThan $largerFileSize $Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$largerFileSize-" $Headers.ContainsKey('Content-Range') | Should -BeFalse } It "Invoke-RestMethod -Resume resumes downloading from <bytes> bytes" -TestCases @( @{bytes = 4} @{bytes = 8} @{bytes = 12} @{bytes = 16} ) { param($bytes) # Simulate partial download $uri = Get-WebListenerUrl -Test 'Resume' -TestValue "Bytes/$bytes" $null = Invoke-RestMethod -Uri $uri -OutFile $outFile Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $bytes $response = Invoke-RestMethod -Uri $resumeUri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$bytes-" $Headers.'Content-Range'[0] | Should -BeExactly "bytes $bytes-$($referenceFileSize-1)/$referenceFileSize" } It "Invoke-RestMethod -Resume assumes the file was successfully completed when the local and remote file are the same size." { # Download the entire file $uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'NoResume' $null = Invoke-RestMethod -Uri $uri -OutFile $outFile $fileSize = Get-Item $outFile | Select-Object -ExpandProperty Length $response = Invoke-RestMethod -Uri $resumeUri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume $outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile $outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize $Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true' $Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$fileSize-" $Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize" } } Context "Invoke-RestMethod retry tests" { It "Invoke-RestMethod can retry - specified number of times - error 304" { $uri = Get-WebListenerUrl -Test 'Retry' $sessionId = (New-Guid).Guid $body = @{ sessionid = $sessionId; failureCode = 304; failureCount = 2 } $commandStr = "Invoke-RestMethod -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body" $result = ExecuteWebCommand -command $commandStr $result.output.failureResponsesSent | Should -Be 2 $result.output.sessionId | Should -BeExactly $sessionId } It "Invoke-RestMethod can retry with POST" { $uri = Get-WebListenerUrl -Test 'Retry' $sessionId = (New-Guid).Guid $body = @{ sessionid = $sessionId; failureCode = 404; failureCount = 1 } $commandStr = "Invoke-RestMethod -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body" $result = ExecuteWebCommand -command $commandStr $result.output.failureResponsesSent | Should -Be 1 $result.output.sessionId | Should -BeExactly $sessionId } It "Invoke-RestMethod respects the Retry-After header value in 429 status" { $Query = @{ statusCode = 429 reposnsephrase = 'Too Many Requests' contenttype = 'application/json' body = '{"message":"oops"}' headers = '{"Retry-After":"1"}' } $uri = Get-WebListenerUrl -Test 'Response' -Query $Query $verboseFile = Join-Path $TestDrive -ChildPath verbose.txt $result = Invoke-RestMethod -Uri $uri -MaximumRetryCount 1 -RetryIntervalSec 3 -SkipHttpErrorCheck -Verbose 4>$verbosefile $verboseFile | Should -FileContentMatch 'Retrying after interval of 1 seconds. Status code for previous attempt: TooManyRequests' } It "Invoke-RestMethod ignores the Retry-After header value NOT in 429 status" { $Query = @{ statusCode = 409 reposnsephrase = 'Conflict' contenttype = 'application/json' body = '{"message":"oops"}' headers = '{"Retry-After":"1"}' } $uri = Get-WebListenerUrl -Test 'Response' -Query $Query $verboseFile = Join-Path $TestDrive -ChildPath verbose.txt $result = Invoke-RestMethod -Uri $uri -MaximumRetryCount 1 -RetryIntervalSec 3 -SkipHttpErrorCheck -Verbose 4>$verbosefile $verboseFile | Should -FileContentMatch 'Retrying after interval of 3 seconds. Status code for previous attempt: Conflict' } } } Describe "Validate Invoke-WebRequest and Invoke-RestMethod -InFile" -Tags "Feature", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener } AfterAll { $ProgressPreference = $oldProgress } Context "InFile parameter negative tests" { BeforeAll { $uri = Get-WebListenerUrl -Test 'Post' $testCases = @( #region INVOKE-WEBREQUEST @{ Name = 'Validate error for Invoke-WebRequest -InFile null' ScriptBlock = {Invoke-WebRequest -Uri $uri -Method Post -InFile $null} ExpectedFullyQualifiedErrorId = 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } @{ Name = 'Validate error for Invoke-WebRequest -InFile ""' ScriptBlock = {Invoke-WebRequest -Uri $uri -Method Post -InFile ""} ExpectedFullyQualifiedErrorId = 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } @{ Name = 'Validate error for Invoke-WebRequest -InFile' ScriptBlock = {Invoke-WebRequest -Uri $uri -Method Post -InFile} ExpectedFullyQualifiedErrorId = 'MissingArgument,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } @{ Name = "Validate error for Invoke-WebRequest -InFile $TestDrive\content.txt" ScriptBlock = {Invoke-WebRequest -Uri $uri -Method Post -InFile $TestDrive\content.txt} ExpectedFullyQualifiedErrorId = 'PathNotFound,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } @{ Name = "Validate error for Invoke-WebRequest -InFile $TestDrive" ScriptBlock = {Invoke-WebRequest -Uri $uri -Method Post -InFile $TestDrive} ExpectedFullyQualifiedErrorId = 'WebCmdletInFileNotFilePathException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' } #endregion #region INVOKE-RESTMETHOD @{ Name = "Validate error for Invoke-RestMethod -InFile null" ScriptBlock = {Invoke-RestMethod -Uri $uri -Method Post -InFile $null} ExpectedFullyQualifiedErrorId = 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } @{ Name = "Validate error for Invoke-RestMethod -InFile ''" ScriptBlock = {Invoke-RestMethod -Uri $uri -Method Post -InFile ''} ExpectedFullyQualifiedErrorId = 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } @{ Name = "Validate error for Invoke-RestMethod -InFile" ScriptBlock = {Invoke-RestMethod -Uri $uri -Method Post -InFile} ExpectedFullyQualifiedErrorId = 'MissingArgument,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } @{ Name = "Validate error for Invoke-RestMethod -InFile $TestDrive\content.txt" ScriptBlock = {Invoke-RestMethod -Uri $uri -Method Post -InFile $TestDrive\content.txt} ExpectedFullyQualifiedErrorId = 'PathNotFound,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } @{ Name = "Validate error for Invoke-RestMethod -InFile $TestDrive" ScriptBlock = {Invoke-RestMethod -Uri $uri -Method Post -InFile $TestDrive} ExpectedFullyQualifiedErrorId = 'WebCmdletInFileNotFilePathException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' } #endregion ) } It "<Name>" -TestCases $testCases { param ($scriptblock, $expectedFullyQualifiedErrorId) { & $scriptblock } | Should -Throw -ErrorId $ExpectedFullyQualifiedErrorId } } Context "InFile parameter positive tests" { BeforeAll { $filePath = Join-Path $TestDrive test.txt New-Item -Path $filePath -Value "hello=world" -ItemType File -Force $uri = Get-WebListenerUrl -Test 'Post' } It "Invoke-WebRequest -InFile" { $result = Invoke-WebRequest -InFile $filePath -Uri $uri -Method Post $content = $result.Content | ConvertFrom-Json $content.form.hello.Count | Should -Be 1 $content.form.hello[0] | Should -Match "world" } It "Invoke-RestMethod -InFile" { $result = Invoke-RestMethod -InFile $filePath -Uri $uri -Method Post $result.form.hello.Count | Should -Be 1 $result.form.hello[0] | Should -Match "world" } } } Describe "Web cmdlets tests using the cmdlet's aliases" -Tags "CI", "RequireAdminOnWindows" { BeforeAll { $WebListener = Start-WebListener } It "Execute Invoke-WebRequest" { $query = @{ body = "hello" contenttype = 'text/plain' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $result = Invoke-WebRequest $uri $result.StatusCode | Should -Be "200" $result.Content | Should -Be "hello" } It "Execute Invoke-RestMethod" { $query = @{ contenttype = 'application/json' body = @{Hello = "world"} | ConvertTo-Json -Compress } $uri = Get-WebListenerUrl -Test 'Response' -Query $query $result = Invoke-RestMethod $uri $result.Hello | Should -Be "world" } It "Web cmdlets ignore headers with null value" { $query = @{ body = "hello" contenttype = 'text/plain' } $uri = Get-WebListenerUrl -Test 'Response' -Query $query # Core throws if a header has null value. # We ignore such headers so no exception is expected. { Invoke-WebRequest -Uri $uri -Headers @{ "Location" = $null } } | Should -Not -Throw { Invoke-WebRequest -Uri $uri -ContentType $null } | Should -Not -Throw { Invoke-RestMethod -Uri $uri -Headers @{ "Location" = $null } } | Should -Not -Throw { Invoke-RestMethod -Uri $uri -ContentType $null } | Should -Not -Throw } } Describe 'Invoke-WebRequest and Invoke-RestMethod support Cancellation through CTRL-C' -Tags "CI", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener } AfterAll { $ProgressPreference = $oldProgress } function RunWithCancellation { param( [string]$Command = 'Invoke-WebRequest', [string]$Arguments = '', [uri]$Uri, [int]$DelayBeforeStopSimulationMs = 5000, [switch]$WillComplete ) $pwsh = [PowerShell]::Create() $invoke = "`$result = $Command -Uri `"$Uri`" $Arguments" $task = $pwsh.AddScript($invoke).InvokeAsync() $delay = [System.Threading.Tasks.Task]::Delay($DelayBeforeStopSimulationMs) # Simulate CTRL-C as soon as the timeout expires or the main task ends $null = [System.Threading.Tasks.Task]::WaitAny($task, $delay) $task.IsCompleted | Should -Be $WillComplete.ToBool() $pwsh.Stop() # The download stall is normally 30 seconds from the web listener based # on the first slash separated parameter in the -TestValue provided to # Get-WebListenerUrl -test Stall -TestValue duration/content-type. Wait-UntilTrue { [bool]($Task.IsCompleted) } | Should -BeTrue $result = $pwsh.Runspace.SessionStateProxy.GetVariable('result') $pwsh.Dispose() return $result } It 'Invoke-WebRequest: CTRL-C Cancels request before request headers received' { $uri = Get-WebListenerUrl -test Delay -TestValue 30 RunWithCancellation -Uri $uri -DelayBeforeStopSimulationMs 1000 } It 'Invoke-WebRequest: CTRL-C Cancels request after request headers received' { $uri = Get-WebListenerUrl -test Stall -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri } It 'Invoke-WebRequest: HTTPS CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Https -Test Stall -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri -Arguments "-SkipCertificateCheck" } It 'Invoke-WebRequest: Brotli Compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Test StallBrotli -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri } It 'Invoke-WebRequest: Gzip Compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Test StallGzip -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri } It 'Invoke-WebRequest: Gzip Compression CTRL-C Cancels request after request headers with Content-Length' { $uri = Get-WebListenerUrl -Test StallGzip -TestValue '30/application%2fjson' -Query @{ contentLength = $true } RunWithCancellation -Uri $uri } It 'Invoke-WebRequest: Deflate Compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Test StallDeflate -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri } It 'Invoke-WebRequest: HTTPS with Brotli compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Https -Test StallBrotli -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri -Arguments '-SkipCertificateCheck' } It 'Invoke-WebRequest: HTTPS with Gzip compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Https -Test StallGzip -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri -Arguments '-SkipCertificateCheck' } It 'Invoke-WebRequest: HTTPS with Deflate compression CTRL-C Cancels request after request headers' { $uri = Get-WebListenerUrl -Https -Test StallDeflate -TestValue '30/application%2fjson' RunWithCancellation -Uri $uri -Arguments '-SkipCertificateCheck' } It 'Invoke-WebRequest: CTRL-C Cancels file download request after request headers received' { $uri = Get-WebListenerUrl -Test Stall -TestValue '30' $outFile = Join-Path $TestDrive "output.txt" RunWithCancellation -Uri $uri -Arguments "-OutFile $outFile" # No guarantee the file will be present since the D/L is interrupted if (Test-Path -Path $outFile) { Remove-Item -Path $outFile } } It 'Invoke-WebRequest: CTRL-C after stalled file download completes gives entire file' { $uri = Get-WebListenerUrl -test Stall -TestValue '1' $outFile = Join-Path $TestDrive "output.txt" RunWithCancellation -Uri $uri -Arguments "-OutFile $outFile" -WillComplete Get-content -Path $outFile | should -be 'Hello worldHello world' Remove-Item -Path $outFile } It 'Invoke-RestMethod: CTRL-C Cancels request before request headers received' { $uri = Get-WebListenerUrl -test Delay -TestValue 30 RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri -DelayBeforeStopSimulationMs 1000 } It 'Invoke-RestMethod: CTRL-C Cancels request after JSON request headers received' { $uri = Get-WebListenerUrl -test Stall -TestValue '30/application%2fjson' RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri } It 'Invoke-RestMethod: CTRL-C after stalled JSON download processes JSON response' { $uri = Get-WebListenerUrl -test Stall -TestValue '1/application%2fjson' $result = RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri -WillComplete $result.name3 | should -be 'value3' } It 'Invoke-RestMethod: CTRL-C Cancels request after plain request headers received' { $uri = Get-WebListenerUrl -test Stall -TestValue '30' RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri } It 'Invoke-RestMethod: CTRL-C after stalled atom feed download processes atom response' { $uri = Get-WebListenerUrl -test Stall -TestValue '1/application%2fxml' $result = RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri -WillComplete $result.title | should -be 'Atom-Powered Robots Run Amok' } It 'Invoke-RestMethod: CTRL-C Cancels request in XML atom processing' { $uri = Get-WebListenerUrl -test Stall -TestValue '30/application%2fxml' RunWithCancellation -Command 'Invoke-RestMethod' -Uri $uri } } Describe "Web cmdlets Unix Sockets tests" -Tags "CI", "RequireAdminOnWindows" { BeforeAll { $isWin2016 = Test-IsWindows2016 $isWin2012 = Test-IsWinServer2012R2 $skipTests = $isWin2016 -or $isWin2012 Write-Verbose -Verbose -Message "IsWin2016: $isWin2016 - IsWin2012: $isWin2012 - SkipTests: $skipTests" if ($skipTests){ return } try { $unixSocket = Get-UnixSocketName -ErrorAction Stop $WebListener = Start-UnixSocket $unixSocket -ErrorAction Stop } catch { Write-Verbose -Verbose -Message "Exception: $_" $WebListener = $null $skipTests = $true } } It "Execute Invoke-WebRequest with -UnixSocket" { if ($skipTests) { Set-ItResult -Skipped -Because "Unix sockets are not supported on this platform." return } $uri = Get-UnixSocketUri $result = Invoke-WebRequest $uri -UnixSocket $unixSocket $result.StatusCode | Should -Be "200" $result.Content | Should -Be "Hello World Unix Socket." } It "Execute Invoke-RestMethod with -UnixSocket" { if ($skipTests) { Set-ItResult -Skipped -Because "Unix sockets are not supported on this platform." return } $uri = Get-UnixSocketUri $result = Invoke-RestMethod $uri -UnixSocket $unixSocket $result | Should -Be "Hello World Unix Socket." } } Describe 'Invoke-WebRequest and Invoke-RestMethod support OperationTimeoutSeconds' -Tags "CI", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener } AfterAll { $ProgressPreference = $oldProgress } function RunWithNetworkTimeout { param( [ValidateSet('Invoke-WebRequest', 'Invoke-RestMethod')] [string]$Command = 'Invoke-WebRequest', [string]$Arguments = '', [uri]$Uri, [int]$OperationTimeoutSeconds, [switch]$WillTimeout ) $invoke = "$Command -Uri `"$Uri`" $Arguments" if ($PSBoundParameters.ContainsKey('OperationTimeoutSeconds')) { $invoke = "$invoke -OperationTimeoutSeconds $OperationTimeoutSeconds" } $result = ExecuteWebCommand -command $invoke if ($WillTimeout) { $result.Error | Should -Not -BeNullOrEmpty $fqErrorClass = if ($Command -eq 'Invoke-WebRequest') { 'InvokeWebRequestCommand'} else { 'InvokeRestMethodCommand'} $result.Error.FullyQualifiedErrorId | Should -Be "OperationTimeoutReached,Microsoft.PowerShell.Commands.$fqErrorClass" $result.Output | Should -BeNullOrEmpty } else { $result.Error | Should -BeNullOrEmpty $result.Output | Should -Not -BeNullOrEmpty } } It 'Invoke-WebRequest: OperationTimeoutSeconds does not cancel if stalls shorter than timeout but download takes longer than timeout' { $uri = Get-WebListenerUrl -Test Stall -TestValue '2' -Query @{ chunks = 5 } RunWithNetworkTimeout -Uri $uri -OperationTimeoutSeconds 4 } It 'Invoke-WebRequest: OperationTimeoutSeconds cancels if stall lasts longer than OperationTimeoutSeconds value' { $uri = Get-WebListenerUrl -Test Stall -TestValue 30 RunWithNetworkTimeout -Uri $uri -OperationTimeoutSeconds 3 -WillTimeout } It 'Invoke-WebRequest: OperationTimeoutSeconds cancels if stall lasts longer than OperationTimeoutSeconds value for HTTPS/gzip compression' { $uri = Get-WebListenerUrl -Https -Test StallGzip -TestValue 30 RunWithNetworkTimeout -Uri $uri -OperationTimeoutSeconds 3 -WillTimeout -Arguments '-SkipCertificateCheck' } It 'Invoke-RestMethod: OperationTimeoutSeconds does not cancel if stalls shorter than timeout but download takes longer than timeout' { $uri = Get-WebListenerUrl -Test Stall -TestValue '2' -Query @{ chunks = 5 } RunWithNetworkTimeout -Command Invoke-RestMethod -Uri $uri -OperationTimeoutSeconds 4 } It 'Invoke-RestMethod: OperationTimeoutSeconds cancels if stall lasts longer than OperationTimeoutSeconds value' { $uri = Get-WebListenerUrl -Test Stall -TestValue 30 RunWithNetworkTimeout -Command Invoke-RestMethod -Uri $uri -OperationTimeoutSeconds 2 -WillTimeout } It 'Invoke-RestMethod: OperationTimeoutSeconds cancels when doing XML atom processing' { $uri = Get-WebListenerUrl -Test Stall -TestValue '30/application%2fxml' RunWithNetworkTimeout -Command Invoke-RestMethod -Uri $uri -OperationTimeoutSeconds 2 -WillTimeout } It 'Invoke-RestMethod: OperationTimeoutSeconds cancels when doing JSON processing' { $uri = Get-WebListenerUrl -Test Stall -TestValue '30/application%2fjson' RunWithNetworkTimeout -Command Invoke-RestMethod -Uri $uri -OperationTimeoutSeconds 2 -WillTimeout } It 'Invoke-RestMethod: OperationTimeoutSeconds cancels when doing XML atom processing for HTTPS/gzip compression' { $uri = Get-WebListenerUrl -Https -Test StallGzip -TestValue 30/application%2fXML RunWithNetworkTimeout -Command Invoke-RestMethod -Uri $uri -OperationTimeoutSeconds 2 -WillTimeout -Arguments '-SkipCertificateCheck' } } Describe "Invoke-RestMethod should run in the default synchronization context (threadpool)" -Tag "CI" { BeforeAll { $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' $WebListener = Start-WebListener } AfterAll { $ProgressPreference = $oldProgress } It "Invoke-RestMethod works after constructing a WindowsForm object" -Skip:(!$IsWindows) { $env:URI_TO_TEST = Get-WebListenerUrl -Test 'Get' $irmResult = $null try { $pwsh = Join-Path $PSHOME "pwsh" $irmResult = & $pwsh -NoProfile { Add-Type -AssemblyName System.Windows.Forms $null = New-Object System.Windows.Forms.Form Invoke-RestMethod $env:URI_TO_TEST } } finally { $env:URI_TO_TEST = $null } # Simply making it this far is good enough to ensure we didn't hit a deadlock $irmResult | Should -Not -BeNullOrEmpty } }