PowerShell Pester Mock Rest API Calls - powershell

Is there any simple approach on how to mock a Rest API Calls in Pester.
Here is my code, I just need to mock those Rest API Calls in Pester and test them, could someone help me here.
Describe 'Test worldclockapi.com' {
BeforeAll {
$response = Invoke-WebRequest -Method 'GET' -Uri 'http://worldclockapi.com/api/json/utc/now'
$responseContent = $response.Content | ConvertFrom-Json
Write-Output $responseContent
}
It 'It should respond with 200' {
$response.StatusCode | Should -Be 200
}
It 'It should have a null service response' {
$responseContent.serviceResponse | Should -BeNullOrEmpty
}
It 'It should be the right day of the week' {
$dayOfWeek = (Get-Date).DayOfWeek
$responseContent.dayOfTheWeek | Should -Be $dayOfWeek
}
It 'It should be the right year' {
$year = Get-Date -Format 'yyyy'
$responseContent.currentDateTime | Should -BeLike "*$year*"
}
It 'It should be the right month' {
$month = Get-Date -Format 'MM'
$responseContent.currentDateTime | Should -BeLike "*$month*"
}
# These two tests assume you are running this outside daylight savings (during the winter) .. hacky but good way to showcase the syntax ;)
It 'It should not be daylight savings time' {
$responseContent.isDayLightSavingsTime | Should -Not -Be $true
}
It 'It should not be daylight savings time another way' {
$responseContent.isDayLightSavingsTime | Should -BeFalse
}
}

It's probably easiest to use a real response as a template for your mock output.
Invoke-WebRequest -Method 'GET' -Uri 'http://worldclockapi.com/api/json/utc/now' |
Export-Clixml .\response.xml
The above command will serialize a real response from the API to a file.
Now we can import the file to use with our mock. All it takes is to use the Mock command to define our mock.
Mock
-CommandName : command we are mocking
-ParameterFilter: An optional filter to limit mocking behavior only to usages of CommandName where the values of the parameters passed to the command pass the filter. This ScriptBlock must return a boolean value.
-MockWith: A ScriptBlock specifying the behavior that will be used to mock CommandName. The default is an empty ScriptBlock. It is here that we import the file to be our output.
Mock -CommandName Invoke-WebRequest -ParameterFilter { $Method -eq 'GET' } -MockWith { Import-Clixml .\response.xml }
Now when Invoke-WebRequest is called with -Method 'Get' our mock will be called instead and will return our object which we import using Import-CliXml (or other method - json, xml, etc. Note: the mock command must come before any calls to the command we are mocking. Also note that the mock will be called even inside other functions that use the command.
BeforeAll {
Mock -CommandName Invoke-WebRequest -ParameterFilter { $Method -eq 'GET' } -MockWith { Import-Clixml .\response.xml }
# on this next line our mock will be called instead and will return our prepared object
$response = Invoke-WebRequest -Method 'GET' -Uri 'http://worldclockapi.com/api/json/utc/now'
$responseContent = $response.Content | ConvertFrom-Json
Write-Output $responseContent
}

I think Daniel's answer is great, but if you are working on a large or shared repository then you just need to be careful about managing those XML files too. Another option, which I use, is to have one large Json file for all your returned objects using real responses. It can be imported in either BeforeAll or BeforeDiscovery depending on how your tests are structured.
The reason for my supplementary answer is really just to cover error responses too, because it is important to have test cases that show how you deal with a REST call failing. Wrapping Invoke-WebRequest in your own function might be useful for returning personalised errors, handling header responses, and having constants for a site name or an allowed set of API paths. Depending on the version of PowerShell, this is how I might handle a 404, for example.
Context " When a path does not exist in the API" {
BeforeAll {
Mock Invoke-WebRequest {
# Use the actual types returned by your function or directly from Invoke-WebRequest.
if ($PSVersionTable.PSEdition -eq "Desktop") {
$WR = New-MockObject -Type 'System.Net.HttpWebResponse'
$Code = [System.Net.HttpStatusCode]::NotFound
# Use Add-Member because StatusCode is a read-only field on HttpWebResponse
$WR | Add-Member -MemberType NoteProperty -Name StatusCode -Value $Code -Force
$Status = [System.Net.WebExceptionStatus]::ProtocolError
$Ex = [System.Net.WebException]::new("404", $null, $Status, $WR)
}
else {
$Message = [System.Net.Http.HttpResponseMessage]::new()
$Message.StatusCode = [System.Net.HttpStatusCode]::NotFound
$Ex = [Microsoft.PowerShell.Commands.HttpResponseException]::new("404", $Message)
}
throw $Ex
} -ParameterFilter {
$Uri -eq "http://worldclockapi.com/api/json/utc/NEVER" -and
$Method -eq "Get"
}
$GetRestTimeParams = #{
Uri = "http://worldclockapi.com/api/json/utc/NEVER"
Method = "Get"
}
}
It " Get-RestTime should not run successfully" {
{ Get-RestTime #GetRestTimeParams } | Should -Throw
}
It " Get-RestTime should throw a 404 error" {
$ShouldParams = #{
# Use the actual types returned by your function or directly from Invoke-WebRequest.
ExceptionType = [System.Net.WebException]
ExpectedMessage = "404: NotFound"
}
{
Get-RestTime #GetRestTimeParams
} | Should -Throw #ShouldParams
}
}

Related

Download a file, get the status, then execute the file

I've tried invoke-restmethod, new-object and many other methods to achieve what I'm trying to do. Here are the latest two iterations:
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1"
Write-Host "StatusCode:" $req.StatusCode
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1" | Select-Object -Expand StatusCode
Write-Host "StatusCode:" $req
Basically I'm attempting to download another PowerShell script and execute it. So obviously it needs to be synchronous. I also need the status so I can determine if it updated or not.
Here is pseudo code for what I'm trying to accomplish:
try {
download file
} catch {
output error
if (local copy exists) {
log warning that local copy is being used
} else {
log error could not download and no local copy available
exit script
}
}
run script (only after downloading new one if available)
Here is my current code in full:
$param1=$args[0]
if ($param1 -eq "-d" -or $param1 -eq "-D") {
$isDev = $true
}
#todo: Move to config file
$logpath = "c:\company\logs\loginscript"
$scriptpath = "c:\company\scripts\"
$scripturl = "http://downloads.company.com/fls.core.ps1"
$logfile="$(Get-Date -Format "yyyy-MM-dd hhmmss").log"
Function log($message) {
Write-Output "[$(Get-Date -Format "yyyy-MM-dd hhmmss")] $message" | Out-file "$($logpath)\$($logfile)" -append
if ($isDev) { Write-Host "[$(Get-Date -Format "yyyy-MM-dd hhmmss")] $message" }
}
Function createFolder($path) {
if (-!(Test-Path $path)) { New-Item -Type Directory -Path $path }
}
function updateScripts() {
try {
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1"
Write-Host "StatusCode:" $req.StatusCode
} catch {
Write-Host "StatusCode:" $req.StatusCode
if ($req.StatusCode -eq 404) {
log "WARNING: Script not found at $scripturl"
} else {
log "ERROR: Script download error: $req.StatusCode"
}
if (Test-Path "$($scriptpath)\fls.core.ps1") {
log "WARNING: Using local script"
} else {
log "ERROR: Unable to update script and no local script found. Exiting."
exit
}
}
}
#----------------------------------------------#
#---- MAIN CODE BLOCK -------------------------#
#----------------------------------------------#
createFolder $logpath
createFolder $scriptpath
#update scripts
updateScripts
#execute core loginscript
& $scriptpath/fls.core.ps1
$req.StatusCode appears to be null.
Invoke-WebRequest reports errors as statement-terminating errors, which means that no assignment to variable $req (in statement $req = Invoke-WebRequest ...) takes place in case an error occurs.
Instead, unfortunately, if an error occurs, the response object[1] must be gleaned from the [ErrorRecord] instance representing the error, which is available via $Error[0] after the fact, or via $_ in the catch block of a try { ... } catch { ... } statement (adapted from this answer):
try {
Invoke-WebRequest -Uri $scripturl -OutFile "$scriptpath\fls.core.ps1"
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Get the status code...
$statuscode = $_.Exception.Response.StatusCode
# ... and work with it.
# if ($statusCode -eq 404) { ...
} catch {
# Unexpected error, re-throw
throw
}
Strictly speaking, $_.Exception.Response.StatusCode returns a value from an enumeration type, System.Net.HttpStatusCode, not an [int] value, but you can use it like an integer. To return an integer to begin with, append .Value__ or cast to [int].
Note that Invoke-WebRequest is always synchronous; if you download a file (successfully), the call won't return until the download is completed.
[1] As the linked answer explains, the response object contained in the error record is of a different type than the one that Invoke-WebRequest returns in case of success (which requires -PassThru if -OutFile is also specified): The error record's .Exception.Response property contains a System.Net.Http.HttpResponseMessage instance, whereas Invoke-WebRequest returns an instance (derived from) Microsoft.PowerShell.Commands.WebResponseObject, which incorporates an instance of the former type, in its .BaseResponse property.

PowerShell Start-Job not running a Scriptblock

I have been struggling for a few days now with running Start-Job with a -Scriptblock.
If I run this, I get today's date back in Receive-Job:
$sb = {Get-Date}
Start-Job -ScriptBlock $sb
Now, I have a script which does some reporting via 65+ API calls for 8 different statuses. In total, it does 500+ API calls and takes almost 20mins to complete (2-5seconds per API call, x 500), so I am looking to run each foreach block in parallel.
The script works well as a script but not as a Start-Job.
The Scriptblock will get a session token, gets data from API server in a foreach loop and then populates the results into a $Global: variable as it goes.
When I run the job, it "Completes" instantly and Has More Data is False (so I can't see any error messages):
67 Job67 BackgroundJob Completed False localhost foreach ($item....
Any suggestions as to where I am going wrong?
My sample code looks like this:
$sb = {# Get count of Status1
foreach ($item in $Global:GetItems) {
$item1 = $item.id
If ($null -eq $TokenExpiresIn) { .\Get-Token.ps1 }
$Global:TokenExpiresIn = New-TimeSpan -Start $TokenExpiresNow -End $Script:TokenExpiresTime
$Method = "GET"
$Fromitems = $item1
$Status = "foo"
$Limit = "1"
$Fields = "blah"
$URL = "https://$APIServer/apis/v1.1/status?customerId=1&itemId=$Fromitems&status=$Status&fields=$Fields&limit=$Limit"
$Header = #{"Accept" = "*/*"; "Authorization" = "Bearer $SessionToken" }
$itemStatsResponse = Invoke-WebRequest -Method $Method -Uri $URL -Header $Header
$itemStatsJSON = ConvertFrom-Json -InputObject $itemStatsResponse
$itemCount = $itemStatsJSON.metadata.count
$Global:GetItems | Where-Object -Property id -EQ $item1 | Add-Member -MemberType NoteProperty "foo" -Value $itemCount
}
}
I am running the Scriptblock as follows:
Start-Job -Name "foo" -ScriptBlock $sb
I am using PowerShell 7.1. The script as a whole runs successfully through all 500+ API calls, however it's a bit slow, so I am also looking at how I can "milti-thread" my API calls moving forward for better performance.
Thanks in advance for your support/input. The other posts on StackOverflow relating to PowerShell and Scriptblocks have not assisted me thus far.

URL health-check PowerShell script correctly gets HTTP 200 on most sites, but incorrect '0' status code on some...API timeout issue?

I have a URL health-checking PowerShell script which correctly gets an HTTP 200 status code on most of my intranet sites, but a '0' status code is returned on a small minority of them. The '0' code is an API return rather than from the web site itself, according to my research of questions from others who have written similar URL-checking PowerShell scripts. Thinking this must be a timeout issue, where API returns '0' before the slowly-responding web site returns its 200, I've researched yet more questions about this subject area on SO and implemented a suggestion from someone to insert a timeout in the script. The timeout setting though, no matter how high I set the timeout value, doesn't help. I still get the same '0' "response" code from the same web sites even though those web sites are up and running as checked from any regular web browser. Any thoughts on how I could tweak the timeout setting in the script below in order to get the correct 200 response code?
The Script:
$URLListFile = "C:\Users\Admin1\Documents\Scripts\URL Check\URL_Check.txt"
$URLList = Get-Content $URLListFile -ErrorAction SilentlyContinue
#if((test-path $reportpath) -like $false)
#{
#new-item $reportpath -type file
#}
#For every URL in the list
$result = foreach($Uri in $URLList) {
try{
#For proxy systems
[System.Net.WebRequest]::DefaultWebProxy = [System.Net.WebRequest]::GetSystemWebProxy()
[System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
#Web request
$req = [system.Net.WebRequest]::Create($uri)
$req.Timeout=5000
$res = $req.GetResponse()
}
catch {
#Err handling
$res = $_.Exception.Response
}
$req = $null
#Getting HTTP status code
$int = [int]$res.StatusCode
# output a formatted string to capture in variable $result
"$int - $uri"
#Disposing response if available
if($res){
$res.Dispose()
}
}
# output on screen
$result
#output to log file
$result | Set-Content -Path "C:\Users\Admin1\Documents\Scripts\z_Logs\URL_Check\URL_Check_log.txt" -Force
Current output:
200 - http://192.168.1.1/
200 - http://192.168.1.2/
200 - http://192.168.1.250/config/authentication_page.htm
0 - https://192.168.1.50/
200 - http://app1-vip-http.dev.local/
0 - https://CA/certsrv/Default.asp
Perhaps using PowerShell cmdlet Invoke-WebRequest works better for you. It has many more parameters and switches to play around with like ProxyUseDefaultCredentials and DisableKeepAlive
$pathIn = "C:\Users\Admin1\Documents\Scripts\URL Check\URL_Check.txt"
$pathOut = "C:\Users\Admin1\Documents\Scripts\z_Logs\URL_Check\URL_Check_log.txt"
$URLList = Get-Content -Path $pathIn
$result = foreach ($uri in $URLList) {
try{
$res = Invoke-WebRequest -Uri $uri -UseDefaultCredentials -UseBasicParsing -Method Head -TimeoutSec 5 -ErrorAction Stop
$status = [int]$res.StatusCode
}
catch {
$status = [int]$_.Exception.Response.StatusCode.value__
}
# output a formatted string to capture in variable $result
"$status - $uri"
}
# output on screen
$result
#output to log file
$result | Set-Content -Path $pathOut -Force

Measure response time using Invoke-WebRequest similar to curl

I have a curl command which response time by breaking it by each action in invoking a service.
curl -w "#sample.txt" -o /dev/null someservice-call
I want to measure the response time in a similar way using PowerShell's built-in Invoke-WebRequest call. So far I am able to get total response time using Measure-Command. Can someone please help me with this?
Content of sample.txt used in curl:
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_pretransfer: %{time_pretransfer}\n
time_redirect: %{time_redirect}\n
time_starttransfer: %{time_starttransfer}\n
----------\n
time_total: %{time_total}\n
time in milliseconds:
$url = "google.com"
(Measure-Command -Expression { $site = Invoke-WebRequest -Uri $url -UseBasicParsing }).Milliseconds
This seems to do it without any noticable overhead:
$StartTime = $(get-date)
Invoke-WebRequest -Uri "google.com" -UseBasicParsing
Write-Output ("{0}" -f ($(get-date)-$StartTime))
As the other solutions point out, there is a performance catch when using powershell only.
The most efficient solution would probably be to write some c# with the measurements built in. But when it's not properly compiled beforehand, the loading-time will increase dramatically when the C# needs to be compiled.
But there is another way.
Since you can use almost all dotnet constructs within powershell, you can just write the same request and measurement logic within powershell itself.
I have written a small method which should do the trick:
function Measure-PostRequest {
param(
[string] $Url,
[byte[]] $Bytes,
[switch] $Block
)
$content = [Net.Http.ByteArrayContent]::new($bytes);
$client = [Net.Http.HttpClient]::new();
$stopwatch = [Diagnostics.Stopwatch]::new()
$result = $null;
if ($block) {
# will block and thus not allow ctrl+c to kill the process
$stopwatch.Start()
$result = $client.PostAsync($url, $content).GetAwaiter().GetResult()
$stopwatch.Stop()
} else {
$stopwatch.Start()
$task = $client.PostAsync($url, $content)
while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
$result = $task.GetAwaiter().GetResult()
$stopwatch.Stop()
}
[PSCustomObject]#{
Response = $result
Milliseconds = $stopwatch.ElapsedMilliseconds
}
}

PowerShell Write-Output not working in try/catch block?

Below is my Powershell environment (via the Get-Host command):
Name : Windows PowerShell ISE Host
Version : 5.1.15063.608
InstanceId : 46c51405-fc6d-4e36-a2ae-09dbd4069710
UI : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture : en-US
CurrentUICulture : en-US
PrivateData : Microsoft.PowerShell.Host.ISE.ISEOptions
DebuggerEnabled : True
IsRunspacePushed : False
Runspace : System.Management.Automation.Runspaces.LocalRunspace
I have a script that makes a REST request to a resource -- everything works fine.
While testing, I changed the URL to be invalid to see if error messages are written to the output file -- this is where I am confused. It appears that the only way to write exceptions to the log is by using Write-Host. When I use Write-Output the exception messages are NEVER written to the log file. Below is the code I am using:
function rest_request( $method, $uri )
{
# Code here create #req dictionary
try {
$json = Invoke-RestMethod #req
} catch {
# If using Write-Output - Nothing is written to output file???
# MUST use Write-Host for error info to be included in output file - WHY?
Write-Host "* REST Request Error:"
Write-Host $error[0] | Format-List -force
$json = $null
}
return $json
}
function do_something()
{
Write-Output "Start..."
# other functions called here but removed for clarity.
# The other functions contain Write-Output statements
# which are showing in the log file.
# Issue REST request with invalid URL to cause exception ...
$json = rest_request "Get" "https://nowhere.com/rest/api"
Write-Output "Done."
}
do_something | Out-File "D:\temp\log_file.txt" -Append
According to this blog post: using Write-Host should be avoided, and yet, it seems that Write-Host is the only way I can get error messages in the output file.
My question is: How do you write a function that makes a REST request and returns the result if successful, but if an error occurs, write the error message to an output file and return $null?
As I am fairly green when it comes to PowerShell, any advice would be appreciated.
Ok - I gave up completely on redirection and instead wrote a function that writes the parameter to the log file, as follows:
function log_write
{
param
(
[Parameter(ValueFromPipeline=$true)] $piped
)
$piped | Out-File $log_file -Append
}
Note: $log_file is set at initialization as I needed a new log file name for each day of the week.
Then, I replaced ALL Write-Output / Write-Host with log_write, i.e.:
log_write "* REST Request Error:"
$s = $error[0] | Format-List -force
log_write $s
Works like a charm and exceptions messages are written to the log file without any of the previous issues.
As far as I know Write-Host only writes to the host, nothing else.
In your example, do_something does not return anything, so it's no wonder you don't get anything in the log file.
Here is how you could do it, but many different approaches exist:
function Send-RESTRequest {
Param($method, $uri, $logFile)
# Code here create #req dictionary
try {
# make sure exception will be caught
$json = Invoke-RestMethod #req -ErrorAction Stop
} catch {
# send exception details to log file
$_ | Out-File $logFile -Append
$json = $null
}
return $json
}
function Do-Something {
# Issue REST request with invalid URL to cause exception ...
$json = Send-RESTRequest -method "Get" -uri "https://nowhere.com/rest/api" -logFile "D:\temp\log_file.txt"
}
Do-Something