ps/Modules/Alkami.DevOps.Validations/Public/Start-WebTests.ps1

686 lines
45 KiB
PowerShell
Raw Permalink Normal View History

2023-05-30 22:51:22 -07:00
function Start-WebTests {
<#
.SYNOPSIS
Runs web tests on web-tier servers only.
.DESCRIPTION
Site must be available in IIS. Keeps track of glogal pass/fail and returns a bool
.PARAMETER LoginsJson
Json file containing logins to test with. Pulled from Get-LoginsToTest function.
.PARAMETER CoreUrlsJson
Json file containing urls to test with. Pulled from Get-CoreUrlsToTest function.
.PARAMETER SitesToSkip
A list (array) of sites to skip web tests on. Http/https will be trimmed
#>
[CmdletBinding()]
[OutputType([System.Boolean])]
param(
[Parameter(Mandatory)]
[string]$LoginsJson,
[Parameter(Mandatory)]
[string]$CoreUrlsJson,
[Parameter(Mandatory = $false)]
[bool]$SkipAdmin = $false,
[Parameter(Mandatory = $false)]
[bool]$SkipAccountCount = $true,
[Parameter(Mandatory = $false)]
[bool]$SkipWidgetWarmUp = $true,
[Parameter(Mandatory = $false)]
[int]$MaximumSitesToTest = 20,
[Parameter(Mandatory = $false)]
[string[]]$SitesToSkip,
[Parameter(Mandatory = $false)]
[int]$SiteThreads = 4,
[Parameter(Mandatory = $false)]
[int]$WidgetThreads = 8
)
begin {
$previousGlobalVerbosity = $global:VerbosePreference
# Quiet the logs for now.
# $global:VerbosePreference = 'Continue'
}
process {
$logLead = (Get-LogLeadName)
$WebTestBaseName = "Start-WebTests"
(Reset-WebTestLogFolder -BaseLogFileName $WebTestBaseName)
$logFilePath = (Get-WebTestLogPath -LogName $WebTestBaseName)
# Only run on Web-tier servers.
if(!(Test-IsWebServer)) {
"$logLead : Current server is not a Web-tier server. Skipping." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
return $true
}
Import-Module -Name PsRedis -Global
# Variables for setup
$logins = ($LoginsJson | ConvertFrom-Json)
$coreurls = ($CoreUrlsJson | ConvertFrom-Json)
# Get all of the URLs in the file that are on this web server.
$siteNames = ((Get-IISSiteList).Bindings.Host).Where({!(Test-IsCollectionNullOrEmpty $logins.$_)}) | Sort-Object -Unique
$siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String)
"$logLead : Sites available to test:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"$logLead : SitesToSkip: $SitesToSkip" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
if(!(Test-IsCollectionNullOrEmpty $SitesToSkip))
{
# Trim any leading "http" nonsense, if necessary.
$SitesToSkip = $SitesToSkip | ForEach-Object {
try {
if ($_.StartsWith("http")) {
$site = $_
[System.Uri]::new($_).Host
} else { $_ }
}
catch {
"$logLead : $site could not be parsed as a site to skip. It is being ignored." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
Resolve-Error
}
}
$siteNames = $siteNames | Where-Object{$_ -notin $SitesToSkip}
"$logLead : Site Names after exclusions: $siteNames" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
}
# If no sites found that match on this webserver, return.
if(Test-IsCollectionNullOrEmpty $siteNames) {
"$logLead : No sites to test on this web server!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
return $true
}
# Limit the number of sites to test.
$siteNames = ($siteNames | Get-Random -Count $MaximumSitesToTest)
$siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String)
"$logLead : Limiting the number of sites to test to $MaximumSitesToTest. Actual number of sites under test is $($siteNames.Count)." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"$logLead : Limited Site Names:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
$allTestResults = @()
$StopWatch = [system.diagnostics.StopWatch]::StartNew()
"$logLead : Starting Web Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
if($SkipAdmin) {
"$logLead : Skipping Admin Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
}
if($SkipAccountCount) {
"$logLead : Skipping Account Count Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
}
if($SkipWidgetWarmUp) {
"$logLead : Skipping Site Widget Warm Up" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
}
# Output some inputs
"$logLead : Maximum Sites To Test: $MaximumSitesToTest" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"$logLead : Site Threads: $SiteThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"$logLead : Widget Threads: $WidgetThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
# Iterate over the sites and get Synthetic accounts from $logins
$siteTestResult = Invoke-Parallel -Objects $siteNames -ReturnObjects -ThreadPerObject -NumThreads $SiteThreads -Arguments @($logins, $coreurls, $SkipAdmin, $SkipAccountCount, $SkipWidgetWarmUp, $WidgetThreads) -script {
Param(
$siteName,
$inputArguments
)
try {
$logLead = "[Invoke-Parallel `$siteNames ($siteName)]"
$logFilePath = (Get-WebTestLogPath -BankUrl $siteName)
$sb_logins = $inputArguments[0]
$sb_coreurls = $inputArguments[1]
$sb_skipAdmin = $inputArguments[2]
$sb_skipAccountCount = $inputArguments[3]
$sb_skipWidgetWarmUp = $inputArguments[4]
$sb_widgetThreads = $inputArguments[5]
$siteLogins = @($sb_logins.$siteName)
if (Test-IsCollectionNullOrEmpty $siteLogins) {
"$logLead : No logins to find, can't test the site!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
$allTestResults += (Test-Should -Result $adminResponse -Predicate ${function:Test-Passthrough} -Widget "Site Logins" -Route "Logins not available" -Login @{UrlSignature = $siteName; Username = "" } $true "There was no corresponding site entry for running this test against. Can not verify if the site is up or down.")
continue
}
$siteTestResults = @()
if (!$sb_skipWidgetWarmUp) {
# Iterate over all of the widgets to warm them up.
$widgetStartResult = Invoke-Parallel -Objects $siteLogins.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($siteLogins.UrlSignature) -script {
Param(
$widgetName,
$inputArguments
)
try {
$logLead = "[Invoke-Parallel `$siteLogins.Widgets ($widgetName)]"
$urlSignature = $inputArguments[0]
$urlSignature = "https://$urlSignature/"
$status = "unknown"
$logFilePath = (Get-WebTestLogPath -BankUrl $urlSignature)
"$logLead : Widget Warmup $urlSignature$widgetName" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$result = (Invoke-Endpoint -Baseuri $urlSignature -Uri $widgetName)
$status = $result.StatusCode
if ([string]::IsNullOrWhiteSpace($status)) {
$status = $result.LastError
}
if ([string]::IsNullOrWhiteSpace($status)) {
$status = "Unknown"
}
$resultWidgetObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Site Warmup"
ReturnValue = $true
ReturnMessage = "Status code: $status"
Username = $null
Url = $urlSignature
Route = $widgetName
Widget = "Site Warmup"
Parameters = $null
Runtime = $result.TotalRuntime
MachineName = (Get-FullyQualifiedServerName)
})
return $resultWidgetObj
} catch {
Write-Warning "$logLead : Failed to properly warm up widgets."
Write-Warning "$logLead : $_"
$resultWidgetObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Site Warmup"
ReturnValue = $false
ReturnMessage = $_.Exception.Message
Username = $null
Url = $inputArguments[0]
Route = $widgetName
Widget = "Site Warmup"
Parameters = $null
Runtime = $null
MachineName = (Get-FullyQualifiedServerName)
})
return $resultWidgetObj
}
}
$siteTestResults += $widgetStartResult
# Give the machine a break.
Start-Sleep -Seconds 3
}
# Each site base-url only has one admin site url to test
# Let's test that one first so we don't loop it when we loop users
# Cole says these will always be the same for a given FI in the above json
if (!$sb_skipAdmin) {
$adminSignature = ($siteLogins | Select-Object -First 1).AdminUrlSignature
# Test admin variant of URL.
$fakeAdminLogin = @{UrlSignature = $adminSignature; Username = "" } # This is for logging purposes only
$bankAdminUrl = "https://$($adminSignature)"
"$logLead : Testing Admin URL : $bankAdminUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$adminResponse = Invoke-Endpoint -Uri $bankAdminUrl
if ($adminResponse.HasErrors -and !($adminResponse.success)) {
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = $adminResponse.LastError
Username = $null
Url = $authUrl
Route = $null
Widget = "Admin Site Warmup"
Parameters = $null
Runtime = $adminResponse.TotalRuntime
MachineName = (Get-FullyQualifiedServerName)
})
$siteTestResults += $resultObj
"$logLead : Error caught for Admin URL: $authUrl. Skipping tests!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
"$logLead : Exception: $($adminResponse.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
}
if ($adminResponse.Success) {
"$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 200)
$siteTestResults += $testResult
"$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 'Content-Type' 'text/html')
$siteTestResults += $testResult
# This is awful, but it's less awful than the rest of the options. TR
# Could I oneliner it? Yes. Would that be harder to maintain - or even read - in 6 months? Definitely. TR
$Content = $adminResponse.Result.Content
$newLine = [System.Environment]::NewLine
$lines = $Content.Split($newLine)
$iframe = $lines.Where( { $_ -match "<iframe" -and $_ -match "authFrame" })
if (![string]::IsNullOrWhiteSpace($iframe)) {
$attributes = $iframe.Trim() -split " "
$adminAuthFrameSrc = $attributes.Where( { $_ -match "src" }).Split("`"")[1]
Write-Host "$logLead : WARMUP Invoking Admin Auth Url (IPSTS) : $adminAuthFrameSrc"
$authUrl = $adminAuthFrameSrc
try {
$authResponse = Invoke-Endpoint -Uri $authUrl
} catch {
Write-Warning "$logLead : WARMUP Failed to GET Admin Auth Url (IPSTS)"
}
}
}
}
foreach ($login in $siteLogins) {
$bankUrl = "https://$($login.UrlSignature)"
$userName = $login.Username
$bankId = $login.BankIdentifier
$rootBankUrl = $login.RootBankUrl # This is because sub-sites like Darden for USF cache things at the USF hostname level
$userAccountsCount = $login.UserAccountCount
$masqueradingIdentifier = $login.MasqueradeUser
$isProdLogin = $login.IsProd
# Used with the HTML generation below to omit the need for https:// parsing
# $signature = $login.UrlSignature
$WebSession = $null
$successfulAuthentication = $false
$authResponse = $null
"$logLead : bankUrl : $bankUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
"$logLead : userName : $userName" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
"$logLead : bankId : $bankId" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
"$logLead : userAccountsCount : $userAccountsCount" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$stoploop = $false
$retryCount = 0
$maxRetries = 4
$sbStopWatch = [System.Diagnostics.StopWatch]::StartNew()
do {
try {
if ($retryCount -le $maxRetries) {
# Poke nonce token into Redis for this user and get the token.
"$logLead : Poking nonce token into Redis for user $userName" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$nonce = (Set-RedisToken -BankUrl $rootBankUrl -UserName $userName)
# Send a request to the auth service to complete the auth process.
$authUrl = "$bankurl/Authentication/Authenticated"
$body = @{ username = $username; nonce = $nonce; source = 'Orion'; }
if ([string]::IsNullOrWhiteSpace($masqueradingIdentifier)) {
if ($isProdLogin) {
"##teamcity[message text='$bankUrl had a user $username that had a flag set for IsProd but the masquerading user was blank on this record. Check the source file.' status='ERROR']" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"$bankUrl had a user $username that had a flag set for IsProd but the masquerading user was blank on this record. Check the source file." | Tee-OutFile -Append -FilePath $logFilePath | Write-Error
$successfulAuthentication = $false # don't try to do anything useful
$authResponse = @{ Success = $false; } # tell it to not try to log anything on "if ($authResponse.Success)"
$stoploop = $true # leave the do-while
break
} else {
Write-Host "In a non-prod environment; Setting the Masquerading ID to the Bank Id."
$masqueradingIdentifier = $bankId
$body.masqueradingIdentifier = $masqueradingIdentifier
}
# it's ok if non-prod tests don't show masquerading users. If they do, cool. If they don't, meh.
# it matters in prod. SRE-16856
} else {
$body.masqueradingIdentifier = $masqueradingIdentifier
}
"$logLead : Sending request to authUrl : $authUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$authResponse = (Invoke-Endpoint -Uri $authUrl -UseNewSession $true -Method 'Post' -Body $body) <# Set to use 'Post' per DEV-116571 #>
$WebSession = $authResponse.WebSession
if ($authResponse.Success) {
"$logLead : Testing if we authenticated successfully..." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
"$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Authentication" -Route "Authenticated" -Login $login 200)
$successfulAuthentication = $testResult.ReturnValue
$siteTestResults += $testResult
"$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Authentication" -Route "Authenticated" -Login $login 'Content-Type' 'text/html')
$siteTestResults += $testResult
# Test if we landed on /Authentication/ page after attempting to login.
if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/Authentication") {
$successfulAuthentication = $false
"$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
Start-Sleep -Seconds 3
$retryCount = $retryCount + 1
} else {
$stoploop = $true
}
} else {
"$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
Start-Sleep -Seconds 10
$retryCount = $retryCount + 1
}
} else {
$sbStopWatch.Stop()
# Break out because we tried 8 times without success.
$stoploop = $true
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = "Authentication failed after $maxRetries retries."
Username = $userName
Url = $login.UrlSignature
Route = "Authenticated"
Widget = "Authentication"
Parameters = $null
Runtime = [System.TimeSpan]::FromMilliseconds($StopWatch.ElapsedMilliseconds).ToString()
MachineName = (Get-FullyQualifiedServerName)
})
$siteTestResults += $resultObj
}
} catch {
"$logLead : Exception for Auth URL: $authUrl. Exception:`n$($_.Exception.Message)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
Start-Sleep -Seconds 10
$retryCount = $retryCount + 1
}
} while ($stoploop -eq $false)
# ensure it got stopped
$sbStopWatch.Stop()
# Test final auth attempt.
if (!$successfulAuthentication) {
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = "Authentication failed! We landed back on login."
Username = $userName
Url = $login.UrlSignature
Route = "Authenticated"
Widget = "Authentication"
Parameters = $null
Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString()
MachineName = (Get-FullyQualifiedServerName)
})
$siteTestResults += $resultObj
}
# Test if we authenticated.
if ($authResponse.Success) {
# Test if we landed on /AtLogin/ page
if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/AtLogin") {
$successfulAuthentication = $false
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = "Authentication failed! We landed on AtLogin. Has user logged in before? Is this a faux-synthetic?"
Username = $userName
Url = $login.UrlSignature
Route = "Authenticated"
Widget = "AtLogin"
Parameters = $null
Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString()
MachineName = (Get-FullyQualifiedServerName)
})
$siteTestResults += $resultObj
}
if ($successfulAuthentication) {
"$logLead : Testing widgets." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
# Since webSession and Cookie collection can't be serialized going into the job, we must create a simple
# array with HashTable to hold the data and rebuild it inside the Invoke-Parallel.
$session = $WebSession
$cookies = @()
foreach ($cookie in $session.Cookies.GetCookies($authUrl)) {
$cookies += @{Name = $cookie.Name; Value = $cookie.Value; Domain = $cookie.Domain }
}
# Loop over the actual widget URL endpoints. Only the URLs where the widget name matches to the login widget.
$widgetUrlResult = Invoke-Parallel -Objects $login.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($sb_coreUrls, $login, $cookies, $bankUrl, $userAccountsCount, $userName, $sb_skipAccountCount) -script {
Param(
$widget,
$inputArguments
)
try {
$logLead = "[Invoke-Parallel `$login.Widgets ($widget)]"
$widgetResults = @()
$sb_coreUrls = $inputArguments[0]
$sb_widgetUrls = @()
if ($null -ne $sb_coreUrls.$widget) {
$sb_widgetUrls = @($sb_coreUrls.$widget)
}
$sb1_login = $inputArguments[1]
$sb1_WebSessionCookies = $inputArguments[2]
$sb1_bankUrl = $inputArguments[3]
$sb1_userAccountsCount = $inputArguments[4]
$sb1_userName = $inputArguments[5]
$sb1_skipAccountCount = $inputArguments[6]
# Create a new session with the cookies array passed in.
$sb_WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
foreach ($cookie in $sb1_WebSessionCookies) {
# Create a new cookie with the data passed in.
$newCookie = New-Object System.Net.Cookie
$newCookie.Name = $cookie.Name
$newCookie.Value = $cookie.Value
$newCookie.Domain = $cookie.Domain
$sb_WebSession.Cookies.Add($newCookie)
}
if (Test-IsCollectionNullOrEmpty $sb_widgetUrls) {
# We intentionally don't have anything to test here.
# This is okay.
# Also note the very special logging parameter because we don't have anything to do, so put this in the parent/site log
$logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl)
"$logLead : Nothing to test for $widget. This is cool." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
continue
}
$logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl -Widget $widget)
"$logLead : Testing widget [$widget] routes [$($sb_widgetUrls -join ',')]" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
foreach ($url in $sb_widgetUrls) {
$sbStopWatch2 = [System.Diagnostics.StopWatch]::StartNew()
if ([string]::IsNullOrWhiteSpace($url)) {
"$logLead : Found an empty url string for $widget. This indicates that either there was an empty string in the coreurls json array for this widget or something was misread. Continuing to the next route." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
continue
}
"$logLead : Testing URL : $sb1_bankUrl$url" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$result = (Invoke-Endpoint -Baseuri $sb1_bankUrl -Uri $url -WebSession $sb_WebSession)
if ($result.HasErrors -and !($result.success)) {
$sbStopWatch2.Stop()
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = $result.LastError
Username = $sb1_userName
Url = $sb1_bankUrl
Route = $url
Widget = $widget
Parameters = $null
Runtime = $result.TotalRuntime
MachineName = (Get-FullyQualifiedServerName)
})
$widgetResults += $resultObj
"$logLead : Could not test $sb1_bankUrl$url for $widget - Is the widget installed?" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
"$logLead : Exception: $($result.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning
}
if ($result.Success) {
$sbStopWatch2.Stop()
$runtime = $sbStopWatch2.Elapsed.ToString("ss\.fffffff")
$status = $result.StatusCode
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $true
ReturnMessage = "Status code: $status"
Username = $sb1_userName
Url = $sb1_bankUrl
Route = $url
Widget = $widget
Parameters = $null
Runtime = $runtime
MachineName = (Get-FullyQualifiedServerName)
})
$widgetResults += $resultObj
# Only check the account count if the path is MyAccountsV2.
if (!$sb1_skipAccountCount -and $url -eq "/MyAccountsV2/") {
"$logLead : Url is /MyAccountsV2/, Running test Test-HaveAccountCount." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveAccountCount} -Widget $widget -Route $url -Login $sb1_login $sb1_userAccountsCount)
$widgetResults += $testResult
}
"$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveStatusCode} -Widget $widget -Route $url -Login $sb1_login 200)
$widgetResults += $testResult
# Commented out for now. Sub-paths may return JSON, so this test fails.
#$testResult = (Test-Should -Result $result -Predicate ${function:Test-HaveResponseHeader} 'Content-Type' 'text/html;')
#$siteTestResults += $testResult
# If the URL ends with a '/', run the test.
if (($url -split '/')[-1].Length -eq 0) {
"$logLead : Running test Test-HaveContentThatMatches." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose
$testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveContentThatMatches} -Widget $widget -Route $url -Login $sb1_login $url)
$widgetResults += $testResult
}
}
}
return $widgetResults
} catch {
Write-Warning "$logLead : Exception occurred when testing widgets."
Write-Warning "$logLead : $_"
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = $_.Exception.Message
Username = $sb1_userName
Url = $sb1_bankUrl
Route = $null
Widget = $widget
Parameters = $null
Runtime = $null
MachineName = (Get-FullyQualifiedServerName)
})
$widgetResults += $resultObj
return $widgetResults
}
}
$siteTestResults += $widgetUrlResult
}
}
}
# foreach site result, write the aggregate times out by the widget
$hash = @{}
$maxKeyLength = 0
foreach ($result in $siteTestResults) {
# Can't calculate runtimes if you don't have them
if ([string]::IsNullOrWhiteSpace($result.Runtime)) {
continue
}
try {
$widgetName = $result.Widget
$runtime = $result.Runtime
$runtimeSplit = $runtime.Split(':')
switch ($runtimeSplit.Length) {
3 { $runtime = $runtime; break; }
2 { $runtime = "00:$runtime"; break; }
1 { $runtime = "00:00:$runtime"; break; }
}
$runtime = [System.TimeSpan]::Parse($runtime)
if ($null -eq $hash.$widgetName) {
$hash.$widgetName = [System.TimeSpan]::new(0)
}
$hash.$widgetName += $runtime
if ($widgetName.Length -gt $maxKeyLength) {
$maxKeyLength = $widgetName.Length
}
} catch {
Write-Warning "$logLead : Couldn't parse the runtime [$($result.Runtime)] for widget [$($result.Widget)]"
Resolve-Error
}
}
$maxKeyLength += 2
$siteUrl = $siteTestResults[0].Url
"$logLead : Test runtime results for $siteUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
foreach ($key in $hash.Keys) {
"$siteUrl - $($key.PadLeft($maxKeyLength)) - $($hash.$key)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
}
return $siteTestResults
} catch {
Write-Warning "$logLead : Exception occurred while running tests."
Write-Warning "$logLead : $_"
$resultObj = New-Object PSObject -Property $([ordered]@{
FunctionName = "Invoke-Endpoint"
ReturnValue = $false
ReturnMessage = $_.Exception.Message
Username = $sb1_userName
Url = $sb1_bankUrl
Route = $null
Widget = $widget
Parameters = $null
Runtime = $null
MachineName = (Get-FullyQualifiedServerName)
})
$siteTestResults += $resultObj
return $siteTestResults
}
}
$allTestResults += @($siteTestResult)
# Log all test results at once for a pretty table
$resultMessage = (Format-Table -AutoSize -Property @(
@{Label = "Should Test"; Expression = {$_.FunctionName.Replace("Test-","")}; Alignment = "Left"},
@{Label = "Passed?"; Expression = {$_.ReturnValue}; Alignment = "Left"},
@{Label = "Url"; Expression = {$_.Url}; Alignment = "Left"},
@{Label = "Route"; Expression = {$_.Route}; Alignment = "Left"},
@{Label = "Result Message"; Expression = {$_.ReturnMessage}; Alignment = "Left";}
@{Label = "Runtime"; Expression = {$_.Runtime}; Alignment = "Left";}
) -InputObject $allTestResults) | Out-String -Width 300
"`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
$resultMessage.Trim() | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
"`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
<#
Commented out but keeping so we can bring this work into a next cycle.
Per discussion with Brent, Justin, and Cole
The two long javascript and css strings should be put into a ride-along file that we get-content and use inline below for maintainability.
This was demoable but not usable from the agent. Future work would be to generate this on the agent along with parsing all the results
On the agent so we can do BuildProblems on the failing lines as well.
$htmlParams = @{
Title = "Start-WebTests :: All Results"
PreContent = "<h2>All Results</h2><style>tr.error {background-color: #fdd;} table {border: 3px solid #000000;width: 100%;text-align: left;border-collapse: collapse;}table td, table th {border: 1px solid #000000;padding: 5px 4px;}table tbody td {font-size: 13px;}table thead {background: #CFCFCF;background: -moz-linear-gradient(top, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);background: -webkit-linear-gradient(top, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);border-bottom: 3px solid #000000;}table thead th {font-size: 15px;font-weight: bold;color: #000000;text-align: left;}table tfoot {font-size: 14px;font-weight: bold;color: #000000;border-top: 3px solid #000000;}table tfoot td {font-size: 14px;}th.sorted.ascending:after {content: ' \2191';}th.sorted.descending:after {content: ' \2193';}</style><script src='http://code.jquery.com/jquery-latest.min.js'></script><script>!function (t) { t.tablesort = function (e, s) { var i = this; this.`$table = e, this.`$thead = this.`$table.find('thead'), this.settings = t.extend({}, t.tablesort.defaults, s), this.`$sortCells = this.`$thead.length > 0 ? this.`$thead.find('th:not(.no-sort)') : this.`$table.find('th:not(.no-sort)'), this.`$sortCells.on('click.tablesort', function () { i.sort(t(this)) }), this.index = null, this.`$th = null, this.direction = null }, t.tablesort.prototype = { sort: function (e, s) { var i = new Date, n = this, o = this.`$table, l = o.find('tbody').length > 0 ? o.find('tbody') : o, a = l.find('tr').has('td, th'), r = a.find(':nth-child(' + (e.index() + 1) + ')').filter('td, th'), d = e.data().sortBy, h = [], c = r.map(function (s, i) { return d ? 'function' == typeof d ? d(t(e), t(i), n) : d : null != t(this).data().sortValue ? t(this).data().sortValue : t(this).text() }); 0 !== c.length && (this.index !== e.index() ? (this.direction = 'asc', this.index = e.index()) : 'asc' !== s && 'desc' !== s ? this.direction = 'asc' === this.direction ? 'desc' : 'asc' : this.direction = s, s = 'asc' == this.direction ? 1 : -1, n.`$table.trigger('tablesort:start', [n]), n.log('Sorting by ' + this.index + ' ' + this.direction), n.`$table.css('display'), setTimeout(function () { n.`$sortCells.removeClass(n.settings.asc + ' ' + n.settings.desc); for (var o = 0, d = c.length; o < d; o++)h.push({ index: o, cell: r[o], row: a[o], value: c[o] }); h.sort(function (t, e) { return n.settings.compare(t.value, e.value) * s }), t.each(h, function (t, e) { l.append(e.row) }), e.addClass(n.settings[n.direction]), n.log('Sort finished in ' + ((new Date).getTime() - i.getTime()) + 'ms'), n.`$table.trigger('tablesort:complete', [n]), n.`$table.css('display') }, c.length > 2e3 ? 200 : 10)) }, log: function (e) { (t.tablesort.DEBUG || this.settings.debug) && console && console.log && console.log('[tablesort] ' + e) }, destroy: function () { return this.`$sortCells.off('click.tablesort'), this.`$table.data('tablesort', null), null } }, t.tablesort.DEBUG = !1, t.tablesort.defaults = { debug: t.tablesort.DEBUG, asc: 'sorted ascending', desc: 'sorted descending', compare: function (t, e) { return t > e ? 1 : t < e ? -1 : 0 } }, t.fn.tablesort = function (e) { var s, i; return this.each(function () { s = t(this), i = s.data('tablesort'), i && i.destroy(), s.data('tablesort', new t.tablesort(s, e)) }) } }(window.Zepto || window.jQuery);</script>"
PostContent = "Generated $([System.DateTime]::Now.ToString())<script>let tableEl = `$('table'); if (!(`$('thead').length)) { tableEl.prepend('<thead>'); let firstTr = tableEl.children('tbody').children('tr').first().detach().appendTo('thead'); } `$('td:contains(`"False`")').parent('tr').addClass('error'); tableEl.tablesort();</script>"
}
$allTestResults | ConvertTo-Html @htmlParams | Out-File .\all.html
#>
$passedAllTests = $allTestResults.Where({ $_.returnValue -eq $false }).Count -eq 0
"$logLead : Passed all tests? $passedAllTests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
$StopWatch.Stop()
$totalHours = $StopWatch.Elapsed.Hours
$totalMinutes = $StopWatch.Elapsed.Minutes
$totalSecs = $StopWatch.Elapsed.Seconds
"$logLead : Web tests ran for a total of $totalHours hours, $totalMinutes minutes, $totalSecs seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host
return $passedAllTests
}
end {
$global:VerbosePreference = $previousGlobalVerbosity
}
}