helpers/test/Invoke-WUGCredentialE2ETest.ps1
|
<#
.SYNOPSIS End-to-end integration tests for WUG credential creation (REST API, Azure types). .DESCRIPTION Tests every part of the Add-WUGCredential flow against a live WUG server: 1. REST API Basic Auth — Proxmox-style (username + password) 2. REST API OAuth2 Client Credentials — Azure-style (token URL, client ID/secret, scope) 3. Azure credential type — Service Principal (TenantId, ClientId, SecureKey) 4. Credential search by name — Get-WUGCredential with -Type filter 5. Credential property bag validation — Verify correct bags returned for each type 6. Set-WUGCredential (update) — Modify credential properties 7. Cleanup — Delete all test credentials Uses DPAPI vault credentials where available (Azure SP, Proxmox Token, WUG login). Falls back to synthetic test values when vault entries are missing. All test credentials are named with a WUGPS-E2E- prefix and cleaned up automatically. .PARAMETER WUGServer WUG server hostname or IP. If omitted, attempts to read from vault or prompts. .PARAMETER Credential PSCredential for WUG auth. If omitted, attempts to read from vault or prompts. .PARAMETER Port API port. Default 9644. .PARAMETER Protocol http or https. Default https. .PARAMETER TenantId Azure Tenant ID for OAuth2 tests. If omitted, attempts to read from vault. .PARAMETER SkipAzure Skip Azure-specific credential tests (useful if no Azure vault entry exists). .PARAMETER SkipProxmox Skip Proxmox-specific credential tests. .EXAMPLE .\Invoke-WUGCredentialE2ETest.ps1 # Uses vault credentials for everything. .EXAMPLE .\Invoke-WUGCredentialE2ETest.ps1 -WUGServer 192.168.74.74 -SkipAzure .NOTES Author : jason@wug.ninja Created : 2026-03-29 Requires: WhatsUpGoldPS module, DiscoveryHelpers.ps1 #> [CmdletBinding()] param( [string]$WUGServer, [PSCredential]$Credential, [int]$Port = 9644, [ValidateSet('http', 'https')] [string]$Protocol = 'https', [string]$TenantId, [switch]$SkipAzure, [switch]$SkipProxmox ) # ============================================================================ #region Setup # ============================================================================ $ErrorActionPreference = 'Continue' $script:TestResults = [System.Collections.Generic.List[PSCustomObject]]::new() $script:Passed = 0 $script:Failed = 0 $script:Skipped = 0 $script:TestPrefix = "WUGPS-E2E-$(Get-Date -Format 'yyyyMMddHHmmss')" $script:CreatedCredIds = [System.Collections.Generic.List[string]]::new() function Record-Test { param( [string]$Name, [string]$Group, [string]$Status, [string]$Detail = '' ) $script:TestResults.Add([PSCustomObject]@{ Name = $Name Group = $Group Status = $Status Detail = $Detail }) switch ($Status) { 'Pass' { $script:Passed++; $color = 'Green' } 'Fail' { $script:Failed++; $color = 'Red' } 'Skipped' { $script:Skipped++; $color = 'Yellow' } default { $color = 'Gray' } } Write-Host " [$Status] $Name $Detail" -ForegroundColor $color } function Invoke-Test { param( [string]$Name, [string]$Group, [scriptblock]$Test ) try { $null = & $Test Record-Test -Name $Name -Group $Group -Status 'Pass' } catch { Record-Test -Name $Name -Group $Group -Status 'Fail' -Detail $_.Exception.Message } } function Assert-NotNull { param($Value, [string]$Message = 'Value was null or empty') if ($null -eq $Value -or ($Value -is [string] -and [string]::IsNullOrEmpty($Value))) { throw $Message } } function Assert-Equal { param($Expected, $Actual, [string]$Message) if ($Expected -ne $Actual) { throw "$(if ($Message) { $Message + ': ' })Expected '$Expected' but got '$Actual'" } } function Assert-Contains { param([array]$Collection, $Value, [string]$Message) if ($Value -notin $Collection) { throw "$(if ($Message) { $Message + ': ' })Collection does not contain '$Value'" } } function Get-BagValue { param([array]$Bags, [string]$BagName) ($Bags | Where-Object { $_.name -eq $BagName } | Select-Object -First 1).value } #endregion # ============================================================================ #region Load dependencies # ============================================================================ Write-Host "`n============================================================" -ForegroundColor Cyan Write-Host " WUG Credential End-to-End Test Suite" -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan Write-Host " Prefix: $script:TestPrefix" -ForegroundColor Gray Write-Host "" # Load DiscoveryHelpers for vault access $discoveryHelpersPath = Join-Path $PSScriptRoot '..\discovery\DiscoveryHelpers.ps1' if (Test-Path $discoveryHelpersPath) { . $discoveryHelpersPath Write-Host " Loaded DiscoveryHelpers.ps1" -ForegroundColor Green } else { Write-Warning "DiscoveryHelpers.ps1 not found — vault credentials unavailable." } # Load WhatsUpGoldPS module $modulePath = Join-Path $PSScriptRoot '..\..\WhatsUpGoldPS.psd1' if (Test-Path $modulePath) { Import-Module $modulePath -Force -ErrorAction Stop Write-Host " Loaded WhatsUpGoldPS module" -ForegroundColor Green } else { Write-Error "Cannot find WhatsUpGoldPS.psd1 at '$modulePath'"; return } #endregion # ============================================================================ #region Resolve connection credentials from vault # ============================================================================ Write-Host "`n[0/7] Resolving credentials from vault ..." -ForegroundColor Cyan # --- WUG Server Connection --- if (-not $WUGServer -or -not $Credential) { $vaultNames = @('WUG.192.168.74.74', 'WUG.Server') foreach ($vn in $vaultNames) { try { $vaultData = Get-DiscoveryCredential -Name $vn -ErrorAction SilentlyContinue if ($vaultData) { # WUG vault entry is pipe-delimited: Server|Port|Protocol|Username|Password $wugParts = "$vaultData" -split '\|' if ($wugParts.Count -ge 5) { if (-not $WUGServer) { $WUGServer = $wugParts[0] } if (-not $Credential) { $secPw = ConvertTo-SecureString $wugParts[4] -AsPlainText -Force $Credential = [PSCredential]::new($wugParts[3], $secPw) } Write-Host " WUG server loaded from vault: $vn" -ForegroundColor Green break } } } catch { Write-Verbose "Vault '$vn' read failed: $_" } } } if (-not $WUGServer) { $WUGServer = Read-Host 'Enter WUG server hostname or IP' } if (-not $Credential) { $Credential = Get-Credential -Message 'Enter WUG credentials' } # --- Azure Service Principal (for OAuth2 + Azure cred tests) --- $script:AzureCred = $null if (-not $SkipAzure) { if (-not $TenantId) { # Try to auto-detect tenant from vault try { $vaultList = Get-DiscoveryCredential -ErrorAction SilentlyContinue $azEntry = $vaultList | Where-Object { $_.Name -match '^Azure\.' -and $_.Name -match 'ServicePrincipal' } | Select-Object -First 1 if ($azEntry -and $azEntry.Name -match 'Azure\.([^.]+)\.ServicePrincipal') { $TenantId = $Matches[1] Write-Host " Auto-detected Azure TenantId from vault: $TenantId" -ForegroundColor Green } } catch { } } if ($TenantId) { try { $azVaultName = "Azure.$TenantId.ServicePrincipal" $azBundle = Get-DiscoveryCredential -Name $azVaultName -ErrorAction SilentlyContinue if ($azBundle) { # AzureSP vault entry is pipe-delimited: TenantId|AppId|ClientSecret $azParts = "$azBundle" -split '\|' if ($azParts.Count -ge 3) { $secSecret = ConvertTo-SecureString $azParts[2] -AsPlainText -Force $script:AzureCred = [PSCredential]::new("$($azParts[0])|$($azParts[1])", $secSecret) } else { # Single string fallback $secSecret = ConvertTo-SecureString "$azBundle" -AsPlainText -Force $script:AzureCred = [PSCredential]::new("$TenantId|unknown", $secSecret) } Write-Host " Azure SP loaded from vault: $azVaultName" -ForegroundColor Green } } catch { Write-Verbose "Azure vault read failed: $_" } } if (-not $script:AzureCred) { Write-Host " Azure SP not in vault — using synthetic test values" -ForegroundColor Yellow $secSecret = ConvertTo-SecureString 'synthetic-test-secret' -AsPlainText -Force $fakeTenant = if ($TenantId) { $TenantId } else { '00000000-0000-0000-0000-000000000000' } $script:AzureCred = [PSCredential]::new("$fakeTenant|synthetic-app-id", $secSecret) if (-not $TenantId) { $TenantId = $fakeTenant } } } # --- Proxmox Token (for Basic auth REST API test) --- $script:ProxmoxToken = $null if (-not $SkipProxmox) { try { $vaultList = Get-DiscoveryCredential -ErrorAction SilentlyContinue $pveEntry = $vaultList | Where-Object { $_.Name -match '^Proxmox\.' -and $_.Name -match 'Token' } | Select-Object -First 1 if ($pveEntry) { $script:ProxmoxToken = Get-DiscoveryCredential -Name $pveEntry.Name -ErrorAction SilentlyContinue if ($script:ProxmoxToken) { Write-Host " Proxmox token loaded from vault: $($pveEntry.Name)" -ForegroundColor Green } } } catch { } if (-not $script:ProxmoxToken) { Write-Host " Proxmox token not in vault — using synthetic test value" -ForegroundColor Yellow $script:ProxmoxToken = 'user@pve!tokenid=00000000-0000-0000-0000-000000000000' } } #endregion # ============================================================================ #region Connect to WUG # ============================================================================ Write-Host "`n[1/7] Connecting to $WUGServer ..." -ForegroundColor Cyan Invoke-Test -Name 'Connect-WUGServer' -Group 'Connection' -Test { Connect-WUGServer -serverUri $WUGServer -Credential $Credential -Port $Port -Protocol $Protocol -IgnoreSSLErrors -ErrorAction Stop if (-not $global:WUGBearerHeaders) { throw 'No bearer headers after connect' } } if (-not $global:WUGBearerHeaders) { Write-Host "`n FATAL: Authentication failed. Cannot continue." -ForegroundColor Red return } #endregion # ============================================================================ #region REST API — Basic Auth (Proxmox style) # ============================================================================ Write-Host "`n[2/7] REST API credential — Basic Auth (Proxmox style) ..." -ForegroundColor Cyan $script:BasicCredId = $null $script:BasicCredResult = $null $basicCredName = "$($script:TestPrefix)-restapi-basic" if ($SkipProxmox) { Record-Test -Name 'Add-WUGCredential (restapi basic)' -Group 'REST-Basic' -Status 'Skipped' -Detail '-SkipProxmox' } else { Invoke-Test -Name 'Add-WUGCredential (restapi basic)' -Group 'REST-Basic' -Test { $result = Add-WUGCredential -Name $basicCredName ` -Description 'E2E test — REST API Basic Auth' ` -Type restapi ` -RestApiUsername 'api-token' ` -RestApiPassword $script:ProxmoxToken ` -RestApiAuthType '0' ` -RestApiIgnoreCertErrors 'True' ` -Confirm:$false -ErrorAction Stop Assert-NotNull $result 'Add-WUGCredential returned null' $script:BasicCredResult = $result $script:BasicCredId = if ($result.id) { $result.id } elseif ($result.data.idMap.resultId) { $result.data.idMap.resultId } else { throw "No ID in result: $($result | ConvertTo-Json -Compress)" } $script:CreatedCredIds.Add($script:BasicCredId) } # Validate type name came back correctly Invoke-Test -Name 'Validate type = "rest api"' -Group 'REST-Basic' -Test { Assert-NotNull $script:BasicCredId 'No credential ID from previous step' # The creation response should have type "rest api" $creds = Get-WUGCredential -Type restapi -ErrorAction Stop $match = $creds | Where-Object { $_.id -eq $script:BasicCredId } Assert-NotNull $match "Credential ID $($script:BasicCredId) not found in restapi list" Assert-Equal 'rest api' $match.type 'Type name mismatch' } # Validate property bags from creation response # NOTE: GET /credentials/{id} and CredentialTemplate do NOT return property bags. # Only the POST creation response includes them. Invoke-Test -Name 'Validate Basic Auth property bags' -Group 'REST-Basic' -Test { Assert-NotNull $script:BasicCredResult 'No creation result from previous step' $bags = $script:BasicCredResult.propertyBags Assert-NotNull $bags 'No propertyBags in creation response' # Verify AuthType = 0 (Basic) $authType = Get-BagValue -Bags $bags -BagName 'CredRestAPI:Authtype' Assert-Equal '0' $authType 'AuthType should be 0 (Basic)' # Verify Username is set $userName = Get-BagValue -Bags $bags -BagName 'CredRestAPI:Username' Assert-Equal 'api-token' $userName 'Username mismatch' # Verify no OAuth2 bags are present (Basic auth should not include GrantType, TokenUrl, etc.) $grantType = Get-BagValue -Bags $bags -BagName 'CredRestAPI:GrantType' if ($grantType) { throw "Basic auth should not have GrantType bag, got: $grantType" } } # Search by name Invoke-Test -Name 'Search credential by name (restapi)' -Group 'REST-Basic' -Test { $creds = @(Get-WUGCredential -Type restapi -ErrorAction Stop) $match = $creds | Where-Object { $_.name -eq $basicCredName } Assert-NotNull $match "Could not find credential by name '$basicCredName'" } } #endregion # ============================================================================ #region REST API — OAuth2 Client Credentials (Azure style) # ============================================================================ Write-Host "`n[3/7] REST API credential — OAuth2 Client Credentials (Azure style) ..." -ForegroundColor Cyan $script:OAuth2CredId = $null $script:OAuth2CredResult = $null $oauth2CredName = "$($script:TestPrefix)-restapi-oauth2" if ($SkipAzure) { Record-Test -Name 'Add-WUGCredential (restapi oauth2)' -Group 'REST-OAuth2' -Status 'Skipped' -Detail '-SkipAzure' } else { # Extract SP parts from Azure credential $azParts = $script:AzureCred.UserName -split '\|' $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($script:AzureCred.Password) try { $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $oauth2Secret = $plainSecret $plainSecret = $null Invoke-Test -Name 'Add-WUGCredential (restapi oauth2 client_credentials)' -Group 'REST-OAuth2' -Test { $result = Add-WUGCredential -Name $oauth2CredName ` -Description 'E2E test — REST API OAuth2 Client Credentials' ` -Type restapi ` -RestApiAuthType '1' ` -RestApiGrantType '0' ` -RestApiTokenUrl "https://login.microsoftonline.com/$($azParts[0])/oauth2/v2.0/token" ` -RestApiClientId $azParts[1] ` -RestApiClientSecret $oauth2Secret ` -RestApiScope 'https://management.azure.com/.default' ` -Confirm:$false -ErrorAction Stop Assert-NotNull $result 'Add-WUGCredential returned null' $script:OAuth2CredResult = $result $script:OAuth2CredId = if ($result.id) { $result.id } elseif ($result.data.idMap.resultId) { $result.data.idMap.resultId } else { throw "No ID in result" } $script:CreatedCredIds.Add($script:OAuth2CredId) } # Validate property bags from creation response # NOTE: Only creation response includes propertyBags; GET endpoints do not. Invoke-Test -Name 'Validate OAuth2 property bags (from create)' -Group 'REST-OAuth2' -Test { Assert-NotNull $script:OAuth2CredResult 'No creation result from previous step' $bags = $script:OAuth2CredResult.propertyBags Assert-NotNull $bags 'No propertyBags in creation response' # Verify AuthType = 1 (OAuth2) $authType = Get-BagValue -Bags $bags -BagName 'CredRestAPI:Authtype' Assert-Equal '1' $authType 'AuthType should be 1 (OAuth2)' # Verify GrantType = 0 (Client Credentials) $grantType = Get-BagValue -Bags $bags -BagName 'CredRestAPI:GrantType' Assert-Equal '0' $grantType 'GrantType should be 0 (ClientCredentials)' # Verify TokenUrl is set $tokenUrl = Get-BagValue -Bags $bags -BagName 'CredRestAPI:TokenUrl' Assert-NotNull $tokenUrl 'TokenUrl should not be empty' if ($tokenUrl -notmatch 'login\.microsoftonline\.com') { throw "TokenUrl looks wrong: $tokenUrl" } # Verify ClientId is set $clientId = Get-BagValue -Bags $bags -BagName 'CredRestAPI:ClientId' Assert-Equal $azParts[1] $clientId 'ClientId mismatch' # Verify Scope is set $scope = Get-BagValue -Bags $bags -BagName 'CredRestAPI:Scope' Assert-Equal 'https://management.azure.com/.default' $scope 'Scope mismatch' # Verify AuthorizeUrl is empty (not required for client_credentials) $authUrl = Get-BagValue -Bags $bags -BagName 'CredRestAPI:AuthorizeUrl' if ($authUrl) { throw "AuthorizeUrl should be empty for client_credentials, got: $authUrl" } } # Validate it appears in restapi credential list Invoke-Test -Name 'Search OAuth2 credential by name' -Group 'REST-OAuth2' -Test { $creds = @(Get-WUGCredential -Type restapi -ErrorAction Stop) $match = $creds | Where-Object { $_.name -eq $oauth2CredName } Assert-NotNull $match "Could not find credential by name '$oauth2CredName'" Assert-Equal 'rest api' $match.type 'Type should be rest api' } # Validate GrantType is NOT authorization code (the bug we fixed) Invoke-Test -Name 'Verify GrantType != AuthorizationCode (2)' -Group 'REST-OAuth2' -Test { Assert-NotNull $script:OAuth2CredResult 'No creation result' $bags = $script:OAuth2CredResult.propertyBags Assert-NotNull $bags 'No propertyBags in creation response' $grantType = Get-BagValue -Bags $bags -BagName 'CredRestAPI:GrantType' if ($grantType -eq '2') { throw "GrantType is 2 (AuthorizationCode) — should be 0 (ClientCredentials). This is the bug we fixed!" } Assert-Equal '0' $grantType 'GrantType should be 0 (ClientCredentials)' } } #endregion # ============================================================================ #region Azure credential type (Service Principal) # ============================================================================ Write-Host "`n[4/7] Azure credential type (Service Principal) ..." -ForegroundColor Cyan $script:AzureCredId = $null $script:AzureCredResult = $null $azureCredName = "$($script:TestPrefix)-azure-sp" if ($SkipAzure) { Record-Test -Name 'Add-WUGCredential (azure)' -Group 'Azure' -Status 'Skipped' -Detail '-SkipAzure' } else { $azParts = $script:AzureCred.UserName -split '\|' $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($script:AzureCred.Password) try { $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $azureSecret = $plainSecret $plainSecret = $null Invoke-Test -Name 'Add-WUGCredential (azure SP)' -Group 'Azure' -Test { $result = Add-WUGCredential -Name $azureCredName ` -Description 'E2E test — Azure Service Principal' ` -Type azure ` -AzureSecureKey $azureSecret ` -AzureTenantID $azParts[0] ` -AzureClientID $azParts[1] ` -Confirm:$false -ErrorAction Stop Assert-NotNull $result 'Add-WUGCredential returned null' $script:AzureCredResult = $result $script:AzureCredId = if ($result.id) { $result.id } elseif ($result.data.idMap.resultId) { $result.data.idMap.resultId } else { throw "No ID in result" } $script:CreatedCredIds.Add($script:AzureCredId) } Invoke-Test -Name 'Validate Azure credential in type list' -Group 'Azure' -Test { $creds = @(Get-WUGCredential -Type azure -ErrorAction Stop) $match = $creds | Where-Object { $_.id -eq $script:AzureCredId } Assert-NotNull $match "Azure credential ID $($script:AzureCredId) not found in azure type list" } Invoke-Test -Name 'Validate Azure property bags' -Group 'Azure' -Test { Assert-NotNull $script:AzureCredResult 'No creation result from previous step' $bags = $script:AzureCredResult.propertyBags Assert-NotNull $bags 'No propertyBags in creation response' $tenantBag = Get-BagValue -Bags $bags -BagName 'CredAzure:TenantID' Assert-Equal $azParts[0] $tenantBag 'TenantID mismatch' $clientBag = Get-BagValue -Bags $bags -BagName 'CredAzure:ClientID' Assert-Equal $azParts[1] $clientBag 'ClientID mismatch' } } #endregion # ============================================================================ #region Set-WUGCredential (update + verify) # ============================================================================ Write-Host "`n[5/7] Set-WUGCredential — update and verify ..." -ForegroundColor Cyan # Pick the first available test credential to verify Get-by-ID works $updateCredId = if ($script:BasicCredId) { $script:BasicCredId } elseif ($script:OAuth2CredId) { $script:OAuth2CredId } elseif ($script:AzureCredId) { $script:AzureCredId } else { $null } if ($updateCredId) { Invoke-Test -Name 'Get-WUGCredential (by ID)' -Group 'Update' -Test { $byId = Get-WUGCredential -CredentialId $updateCredId -ErrorAction Stop Assert-NotNull $byId 'Get-WUGCredential by ID returned null' Assert-Equal $updateCredId $byId.id 'Credential ID mismatch' } } else { Record-Test -Name 'Get-WUGCredential (by ID)' -Group 'Update' -Status 'Skipped' -Detail 'No test credential available' } #endregion # ============================================================================ #region Cross-type search validation # ============================================================================ Write-Host "`n[6/7] Cross-type search validation ..." -ForegroundColor Cyan Invoke-Test -Name 'Get-WUGCredential (all types, no filter)' -Group 'Search' -Test { $all = Get-WUGCredential -Limit 100 -ErrorAction Stop Assert-NotNull $all 'No credentials returned' # Verify our test credentials appear in the unfiltered list foreach ($id in $script:CreatedCredIds) { $found = $all | Where-Object { $_.id -eq $id } Assert-NotNull $found "Test credential ID $id not found in unfiltered list" } } if (-not $SkipProxmox -and $script:BasicCredId) { Invoke-Test -Name 'Get-WUGCredential (restapi filter finds basic)' -Group 'Search' -Test { $restCreds = @(Get-WUGCredential -Type restapi -ErrorAction Stop) $match = $restCreds | Where-Object { $_.id -eq $script:BasicCredId } Assert-NotNull $match "Basic auth cred not in restapi filter results" } } if (-not $SkipAzure -and $script:OAuth2CredId) { Invoke-Test -Name 'Get-WUGCredential (restapi filter finds oauth2)' -Group 'Search' -Test { $restCreds = @(Get-WUGCredential -Type restapi -ErrorAction Stop) $match = $restCreds | Where-Object { $_.id -eq $script:OAuth2CredId } Assert-NotNull $match "OAuth2 cred not in restapi filter results" } } if (-not $SkipAzure -and $script:AzureCredId) { Invoke-Test -Name 'Get-WUGCredential (azure filter excludes restapi)' -Group 'Search' -Test { $azCreds = @(Get-WUGCredential -Type azure -ErrorAction Stop) # Azure filter should include Azure cred but NOT restapi creds $azMatch = $azCreds | Where-Object { $_.id -eq $script:AzureCredId } Assert-NotNull $azMatch "Azure cred not in azure filter" if ($script:OAuth2CredId) { $wrongMatch = $azCreds | Where-Object { $_.id -eq $script:OAuth2CredId } if ($wrongMatch) { throw "OAuth2 restapi cred appeared in azure type filter — type isolation broken" } } } } #endregion # ============================================================================ #region Cleanup # ============================================================================ Write-Host "`n[7/7] Cleanup — deleting test credentials ..." -ForegroundColor Cyan foreach ($credId in $script:CreatedCredIds) { Invoke-Test -Name "Set-WUGCredential (delete $credId)" -Group 'Cleanup' -Test { Set-WUGCredential -CredentialId $credId -Remove -Confirm:$false -ErrorAction Stop | Out-Null } } # Verify cleanup Invoke-Test -Name 'Verify all test credentials deleted' -Group 'Cleanup' -Test { $remaining = @(Get-WUGCredential -Limit 250 -ErrorAction Stop) foreach ($credId in $script:CreatedCredIds) { $leftover = $remaining | Where-Object { $_.id -eq $credId } if ($leftover) { throw "Credential ID $credId still exists after deletion" } } } #endregion # ============================================================================ #region Summary # ============================================================================ Write-Host "`n============================================================" -ForegroundColor Cyan Write-Host " CREDENTIAL E2E TEST RESULTS" -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan $script:TestResults | Format-Table -AutoSize -Property @( @{ Label = 'Group'; Expression = { $_.Group }; Width = 14 } @{ Label = 'Test'; Expression = { $_.Name } } @{ Label = 'Status'; Expression = { $_.Status } } @{ Label = 'Detail'; Expression = { $_.Detail } } ) $total = $script:Passed + $script:Failed + $script:Skipped Write-Host " Total: $total | " -NoNewline Write-Host "Pass: $($script:Passed)" -ForegroundColor Green -NoNewline Write-Host " | " -NoNewline Write-Host "Fail: $($script:Failed)" -ForegroundColor $(if ($script:Failed -gt 0) { 'Red' } else { 'Green' }) -NoNewline Write-Host " | " -NoNewline Write-Host "Skipped: $($script:Skipped)" -ForegroundColor Yellow Write-Host "" if ($script:Failed -gt 0) { Write-Host " FAILURES:" -ForegroundColor Red $script:TestResults | Where-Object { $_.Status -eq 'Fail' } | ForEach-Object { Write-Host " [$($_.Group)] $($_.Name): $($_.Detail)" -ForegroundColor Red } Write-Host "" } # Return results for programmatic use $script:TestResults #endregion # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB8n744QB8tQgYb # Cf0P5V3WFNMsXbykANMLLKOVZOhGA6CCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO # +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw # MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp # Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb # hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT # cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb # NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP # XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr # VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP # ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv # kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM # cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls # 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR # a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6 # MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E # BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg # v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT # SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh # 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw # uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO # QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD # TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G # CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp # bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290 # IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g # UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB # jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe # AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV # oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk # 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh # P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41 # aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ # ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/ # 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g # Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA # QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B # Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD # AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG # Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n # Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu # cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG # 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW # 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh # rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp # mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc # ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE # Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz # NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw # 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6 # E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6 # xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt # g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+ # MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw # WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj # dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB # bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW # kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw # zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1 # DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb # w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1 # Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k # dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J # BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d # n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs # CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU # q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE # LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW # gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G # aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC # ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT # aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz # Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG # SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3 # wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9 # vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd # vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6 # LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO # WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7 # nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6 # g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7 # z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G # CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT # CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh # of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA # xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv # MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps # 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF # 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi # UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM # CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP # pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU # G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+ # sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI # GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+ # IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M # yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2 # th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj # V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2 # Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze # GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG # 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N # Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT # etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW # kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g # VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN # +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo # me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/ # /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT # MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8 # DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp # JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5 # 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49 # kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P # WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y # ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP # 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O # 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg # iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU # BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF # kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+ # zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l # wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl # RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2 # zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg # baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC # IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCCCucpo3+rGqHL3mz2pXNH+1W976jrUBNtYaQNEdhCP4TANBgkqhkiG9w0BAQEF # AASCAgAt03aystI/Zz+/Nf/CfX/jgcLBWS8aeO7aIlFcS0s8kc6sm3W5FNUImD8B # xnZoUZnoD0SUmtTlK5fEFq0Lb3G5ed7PYEg1Wll9W2VetNdS6QR3V+JFKBnA+0iD # t0kQUahTLc87p6gEh+HnV1SCA4049dBeByUQwH7E1OW5xVsWGisTO5DDTaapOqps # oWCAf/K/wGVqpySResN3ol9BoWKeOmllJ1OkwVVDfPEbK1F5yy8MvTZa1AKOLqDI # ncO33y3ULl53mHnQDzxIsOEnBFn+e++iJTFd1YUwZ9C/Tw1yiL+vI5bMbvMbXKGC # cYLiChUeWecDgeROgG69U57l0DX8Yrg+C/N839fFvVx+JzGrTuh71V+8U3V/MPva # c6/NBx5xcRYpRB+j2Y6irPnM9flFC6SqEwNs8iiOPaJZO1bHebY/Y6v1EM0S+0FZ # 52t09fxhDdydbhNRPnWJ5lIuvnC6c0paNLgWmiBgZaxEzxA47JqxKy50OsAyUNHM # mOuPu16zormgWAnxBp+AgK1PBABSRmhyswNJTJO2jRqKTPZJFsDHSTVGeCepMLQY # YKDzP1Q8/0XSIcGI/P+AfG/kV6KAe7ebV62nRzbLxNPbehlnQLw9pDpys8YIrIYj # 1hZH3rYAsb8r8ngOoPITPHbGZcGBVUF5sTGDSd5XtbrA5PFHo6GCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA0MjAwMDAwMzZaMC8GCSqGSIb3DQEJBDEiBCCsGhsS # JoYyi+7hBMnBLRFwHH1JEgEIaUHSzTFd6kF4JjANBgkqhkiG9w0BAQEFAASCAgC+ # aV/gU9xUYz+WN8d4mlOmGjPTb3iCLsJ7oYYthrh1T+8V2gqfdxmiyxE8843K+KgA # viXZ+K57SjYY3BleztqYpo4sSgi/KKzyJSpnI0x2jlnzuYxljzZUJEvWznU8UgLY # i0whfFQVpXF01ILdcQppd6BHtHczpupck3ItBXxGeOKehkumOzIcJ5430w4+bDMm # 4agqZwwR20B/kHjadJ4gUQcvotukSVe+vS3mbbQspriRyoUPgFW11KrVKoVNcB+0 # aoRalCIRvG3ZxlMh82IoOQ5sQfgpMrVfUCmrhv7xJyVOTYsx7EZpLXVP6jqBziIa # wxZsvNr2mtdoYJTSe+rWWLCF46k6vLjRAxa+X/p8FIXEmHypj1my3afStE7ETYJT # b3LcM4o2445SJEtbZOrlrVDF9wZD7Rrd91nJ7PBVE0pbCubeV3cGb1pvcsUuRHys # SDnZ6rlMYGTSm0szyYSkDaY68gKmHTY2QcYCVKaPHvq7zoD15Cvg/wfE/Umzhqw5 # XeYU5VKc68NNiIQ8gLeVt7DcvjZH3+6IBIept6fGHJbQXs/HNwdM5TkfKTSdHAxj # 6pkS/sRkNP8YQMKzorwjmPNTDEqSUDQYy6OVwtCtxN0hjDDIGad6cR1nbkNnDmeT # 3YY9ann0GbNqtQq1pSFoyUGbpXbUjIvhMxIL0CliEQ== # SIG # End signature block |