Extensions/PowerShellCmdletsExtensions.Tests.ps1

Param (
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]
    $Server,

    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]
    $User,

    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]
    $Password
)

BeforeAll {
    function Test-ObjectsAreEqual {
        [CmdletBinding()]
        [OutputType([bool])]
        param (
            [Parameter(Mandatory = $true)]
            [AllowNull()]
            [object]
            $Actual,

            [Parameter(Mandatory = $true)]
            [AllowNull()]
            [object]
            $Expected,

            [Parameter(Mandatory = $false)]
            [type]
            $ExpectedType,

            [Parameter(Mandatory = $false)]
            [int]
            $Depth = 10
        )

        process {
            $result = $true

            if ($null -ne $ExpectedType -and $Actual -IsNot $ExpectedType) {
                Write-Verbose -Message "The actual object's type [$($Actual.GetType())] doesn't match the expected one [$ExpectedType]"
                $result = $false
            }
            elseif ($Expected.GetType().IsPrimitive -or $Expected -Is [string]) {
                Write-Verbose -Message "Primitive type [$($Expected.GetType())] found"
                $result = ($Actual -eq $Expected)
            }
            else {
                Write-Verbose -Message "Complex type [$($Expected.GetType())] found"
                $expectedProperties = $Expected | Get-Member -MemberType Properties

                foreach ($property in $expectedProperties) {
                    Write-Verbose -Message "Asserting property $($property.Name)"

                    $expectedValue = $Expected | Select-Object -ExpandProperty $property.Name
                    $actualValue = $Actual | Select-Object -ExpandProperty $property.Name

                    Write-Verbose -Message "Expected value = $expectedValue"
                    Write-Verbose -Message "Actual value = $actualValue"

                    if ($expectedValue -eq $actualValue) {
                        Write-Verbose -Message "Values are equal"
                        continue
                    }
                    elseif ($Depth -eq -1) {
                        $result = $true
                    }
                    else {
                        $areObjectsEqual = Test-ObjectsAreEqual -Expected $expectedValue -Actual $actualValue -Depth ($Depth - 1)
                        if (!$areObjectsEqual) {
                            $result = $false
                            break
                        }
                    }
                }
            }

            return $result
        }
    }
}

Describe 'PowerShell Cmdlets Extensions Tests' {
    BeforeAll {
        . (Join-Path -Path $PSScriptRoot -ChildPath 'PowerShellCmdletsExtensions.ps1')

        $certificateThumbprint = $null

        if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
            $initialMaxServicePointIdleTime = [System.Net.ServicePointManager]::MaxServicePointIdleTime
            $maxServicePointIdleTimeInMilliseconds = 1

            [System.Net.ServicePointManager]::MaxServicePointIdleTime = $maxServicePointIdleTimeInMilliseconds

            $certificateThumbprint = Get-TlsCertificateThumbprintFromRemoteHost -RemoteHostName $Server
        }
    }

    AfterAll {
        if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
            [System.Net.ServicePointManager]::MaxServicePointIdleTime = $initialMaxServicePointIdleTime
        }
    }

    Context 'Invoke-WebRequestX Tests' {
        It 'Should skip all Certificate checks when SkipCertificateCheck parameter is passed with $true value' {
            # Arrange
            $localVarBytes = [System.Text.Encoding]::UTF8.GetBytes($User + ':' + $Password)
            $localVarBase64Text = [Convert]::ToBase64String($localVarBytes)
            $headers = @{
                'Authorization' = "Basic $localVarBase64Text"
                'Accept' = 'application/json'
            }

            $secureStringPassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
            $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $secureStringPassword

            $invokeWebRequestParams = @{
                'Uri' = "https://$Server/api/session"
                'Method' = 'POST'
                'Headers' = $headers
                'ErrorAction' = 'Stop'
                'UseBasicParsing' = $true
                'Credential' = $credential
            }

            # Act
            if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
                [CustomCertificatesValidator]::AddCertificateToCache($certificateThumbprint)
            }

            $result = Invoke-WebRequestX -InvokeParams $invokeWebRequestParams -SkipCertificateCheck $true

            # Assert
            $result | Should -Not -Be $null
            $result.StatusCode | Should -Be 201
            $result.Content | Should -Not -BeNullOrEmpty
            $result.Headers | Should -Not -Be $null
            $result.Headers['Content-Type'] | Should -Be 'application/json'
            $result.Headers['vmware-api-session-id'] | Should -Not -BeNullOrEmpty
        }

        It 'Should throw an exception with the expected message when SkipCertificateCheck parameter is passed with $false value' {
            # Arrange
            $localVarBytes = [System.Text.Encoding]::UTF8.GetBytes($User + ':' + $Password)
            $localVarBase64Text = [Convert]::ToBase64String($localVarBytes)
            $headers = @{
                'Authorization' = "Basic $localVarBase64Text"
                'Accept' = 'application/json'
            }

            $secureStringPassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
            $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $secureStringPassword

            $invokeWebRequestParams = @{
                'Uri' = "https://$Server/api/session"
                'Method' = 'POST'
                'Headers' = $headers
                'ErrorAction' = 'Stop'
                'UseBasicParsing' = $true
                'Credential' = $credential
            }

            if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
                [CustomCertificatesValidator]::RemoveCertificateFromCache($certificateThumbprint) | Out-Null
            }

            $corePowerShellExpectedExceptionMessage = 'The SSL connection could not be established, see inner exception.'
            $desktopPowerShellExpectedExceptionMessage = 'The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.'
            $expectedInnerExceptionMessage = 'The remote certificate is invalid according to the validation procedure*'

            # Act
            $actualError = $null
            try {
                Invoke-WebRequestX -InvokeParams $invokeWebRequestParams -SkipCertificateCheck $false
            } catch {
                $actualError = $_
            }

            # Assert
            $actualError | Should -Not -Be $null
            $actualError.Exception | Should -Not -Be $null

            if ($Global:PSVersionTable.PSEdition -eq 'Core') {
                $actualError.Exception.Message | Should -Be $corePowerShellExpectedExceptionMessage
            } else {
                $actualError.Exception.Message | Should -Be $desktopPowerShellExpectedExceptionMessage
            }

            $actualError.Exception.InnerException | Should -Not -Be $null
            $actualError.Exception.InnerException.Message | Should -BeLike $expectedInnerExceptionMessage
        }
    }

    Context 'Invoke-RestMethodX Tests' {
        It 'Should skip all Certificate checks when SkipCertificateCheck parameter is passed with $true value' {
            # Arrange
            $localVarBytes = [System.Text.Encoding]::UTF8.GetBytes($User + ':' + $Password)
            $localVarBase64Text = [Convert]::ToBase64String($localVarBytes)
            $headers = @{
                'Authorization' = "Basic $localVarBase64Text"
                'Accept' = 'application/json'
            }

            $secureStringPassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
            $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $secureStringPassword

            $invokeWebRequestParams = @{
                'Uri' = "https://$Server/api/session"
                'Method' = 'POST'
                'Headers' = $headers
                'ErrorAction' = 'Stop'
                'UseBasicParsing' = $true
                'Credential' = $credential
            }

            if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
                [CustomCertificatesValidator]::AddCertificateToCache($certificateThumbprint)
            }

            $sessionId = Invoke-WebRequestX -InvokeParams $invokeWebRequestParams -SkipCertificateCheck $true
            $headers['vmware-api-session-id'] = $sessionId -Replace '"', ''

            $invokeRestMethodParams = @{
                'Uri' = "https://$Server/rest/appliance/system/version"
                'Method' = 'GET'
                'Headers' = $headers
            }

            # Act
            $result = Invoke-RestMethodX -InvokeParams $invokeRestMethodParams -SkipCertificateCheck $true

            # Assert
            $result | Should -Not -Be $null
            $result.value | Should -Not -Be $null
            $result.value.version | Should -Not -Be $null
            [System.Version] $result.value.version | Should -Not -Be $null
        }

        It 'Should throw an exception with the expected message when SkipCertificateCheck parameter is passed with $false value' {
            # Arrange
            $localVarBytes = [System.Text.Encoding]::UTF8.GetBytes($User + ':' + $Password)
            $localVarBase64Text = [Convert]::ToBase64String($localVarBytes)
            $headers = @{
                'Authorization' = "Basic $localVarBase64Text"
                'Accept' = 'application/json'
            }

            $invokeRestMethodParams = @{
                'Uri' = "https://$Server/rest/appliance/system/version"
                'Method' = 'GET'
                'Headers' = $headers
            }

            if ($Global:PSVersionTable.PSEdition -eq 'Desktop') {
                [CustomCertificatesValidator]::RemoveCertificateFromCache($certificateThumbprint) | Out-Null
            }

            $corePowerShellExpectedExceptionMessage = 'The SSL connection could not be established, see inner exception.'
            $desktopPowerShellExpectedExceptionMessage = 'The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.'
            $expectedInnerExceptionMessage = 'The remote certificate is invalid according to the validation procedure*'

            # Act
            $actualError = $null
            try {
                Invoke-RestMethodX -InvokeParams $invokeRestMethodParams -SkipCertificateCheck $false
            } catch {
                $actualError = $_
            }

            # Assert
            $actualError | Should -Not -Be $null
            $actualError.Exception | Should -Not -Be $null

            if ($Global:PSVersionTable.PSEdition -eq 'Core') {
                $actualError.Exception.Message | Should -Be $corePowerShellExpectedExceptionMessage
            } else {
                $actualError.Exception.Message | Should -Be $desktopPowerShellExpectedExceptionMessage
            }

            $actualError.Exception.InnerException | Should -Not -Be $null
            $actualError.Exception.InnerException.Message | Should -BeLike $expectedInnerExceptionMessage
        }
    }

    Context 'Get-PlainTextPassword Tests' {
        It 'Should retrieve the password as plain text from the SecureString' {
            # Arrange
            $expected = 'MyTestPassword'
            $secureStringPassword = ConvertTo-SecureString -String $expected -AsPlainText -Force

            # Act
            $actual = Get-PlainTextPassword -Password $secureStringPassword

            # Assert
            $actual | Should -Be $expected
        }
    }

    Context 'ConvertFrom-JsonX Tests' {
        It 'Should convert the specified JSON to the expected PSCustomObject' {
            # Arrange
            $vmInfoJson = @"
            {
                "instant_clone_frozen": false,
                "guest_OS": "WINDOWS_SERVER_2019",
                "power_state": "POWERED_OFF",
                "name": "testvm",
                "boot_devices": [],
                "custom_vm_props": ["custom_vm_prop_one", "custom_vm_prop_two"],
                "custom_vm_array": ["custom_vm_array_element"],
                "hardware": {
                    "upgrade_policy": "NEVER",
                    "upgrade_status": "NONE",
                    "version": "VMX_19"
                },
                "cdroms": {
                    "16000": {
                        "start_connected": false,
                        "backing": {
                            "device_access_type": "PASSTHRU",
                            "type": "CLIENT_DEVICE"
                        },
                        "allow_guest_control": true,
                        "label": "CD/DVD drive 1",
                        "state": "NOT_CONNECTED",
                        "type": "SATA",
                        "sata": {
                            "bus": 0,
                            "unit": 0
                        }
                    }
                }
            }
"@


            $expected = [PSCustomObject] @{
                'instant_clone_frozen' = $false
                'guest_OS' = 'WINDOWS_SERVER_2019'
                'power_state' = 'POWERED_OFF'
                'name' = 'testvm'
                'boot_devices' = @()
                'hardware' = [PSCustomObject] @{
                    'upgrade_policy' = 'NEVER'
                    'upgrade_status' = 'NONE'
                    'version' = 'VMX_19'
                }
                'cdroms' = [PSCustomObject] @{
                    '16000' = [PSCustomObject] @{
                        'start_connected' = $false
                        'backing' = [PSCustomObject] @{
                            'device_access_type' = 'PASSTHRU'
                            'type' = 'CLIENT_DEVICE'
                        }
                        'allow_guest_control' = $true
                        'label' = 'CD/DVD drive 1'
                        'state' = 'NOT_CONNECTED'
                        'type' = 'SATA'
                        'sata' = [PSCustomObject] @{
                            'bus' = 0
                            'unit' = 0
                        }
                    }
                }
                'custom_vm_props' = @(
                    "custom_vm_prop_one",
                    "custom_vm_prop_two"
                )
                'custom_vm_array' = @("custom_vm_array_element")
            }

            # Act
            $actual = ConvertFrom-JsonX -InputObject $vmInfoJson -Depth 100

            # Assert
            Test-ObjectsAreEqual $actual $expected | Should -BeTrue
        }
    }
}

# SIG # Begin signature block
# MIIohwYJKoZIhvcNAQcCoIIoeDCCKHQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDu63KScQ0w2hBJ
# 05jUMpGtEbFlU73lYU6nQxd2OjUmdaCCDdowggawMIIEmKADAgECAhAIrUCyYNKc
# TJ9ezam9k67ZMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0z
# NjA0MjgyMzU5NTlaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQDVtC9C0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0
# JAfhS0/TeEP0F9ce2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJr
# Q5qZ8sU7H/Lvy0daE6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhF
# LqGfLOEYwhrMxe6TSXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+F
# LEikVoQ11vkunKoAFdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh
# 3K3kGKDYwSNHR7OhD26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJ
# wZPt4bRc4G/rJvmM1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQay
# g9Rc9hUZTO1i4F4z8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbI
# YViY9XwCFjyDKK05huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchAp
# QfDVxW0mdmgRQRNYmtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRro
# OBl8ZhzNeDhFMJlP/2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IB
# WTCCAVUwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+
# YXsIiGX0TkIwHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0P
# AQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAC
# hjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAED
# MAgGBmeBDAEEATANBgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql
# +Eg08yy25nRm95RysQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFF
# UP2cvbaF4HZ+N3HLIvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1h
# mYFW9snjdufE5BtfQ/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3Ryw
# YFzzDaju4ImhvTnhOE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5Ubdld
# AhQfQDN8A+KVssIhdXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw
# 8MzK7/0pNVwfiThV9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnP
# LqR0kq3bPKSchh/jwVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatE
# QOON8BUozu3xGFYHKi8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bn
# KD+sEq6lLyJsQfmCXBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQji
# WQ1tygVQK+pKHJ6l/aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbq
# yK+p/pQd52MbOoZWeE4wggciMIIFCqADAgECAhAOxvKydqFGoH0ObZNXteEIMA0G
# CSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjEwODEwMDAwMDAwWhcNMjMwODEw
# MjM1OTU5WjCBhzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQ
# BgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UEChMMVk13YXJlLCBJbmMuMRUwEwYDVQQD
# EwxWTXdhcmUsIEluYy4xITAfBgkqhkiG9w0BCQEWEm5vcmVwbHlAdm13YXJlLmNv
# bTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAMD6lJG8OWkM12huIQpO
# /q9JnhhhW5UyW9if3/UnoFY3oqmp0JYX/ZrXogUHYXmbt2gk01zz2P5Z89mM4gqR
# bGYC2tx+Lez4GxVkyslVPI3PXYcYSaRp39JsF3yYifnp9R+ON8O3Gf5/4EaFmbeT
# ElDCFBfExPMqtSvPZDqekodzX+4SK1PIZxCyR3gml8R3/wzhb6Li0mG7l0evQUD0
# FQAbKJMlBk863apeX4ALFZtrnCpnMlOjRb85LsjV5Ku4OhxQi1jlf8wR+za9C3DU
# ki60/yiWPu+XXwEUqGInIihECBbp7hfFWrnCCaOgahsVpgz8kKg/XN4OFq7rbh4q
# 5IkTauqFhHaE7HKM5bbIBkZ+YJs2SYvu7aHjw4Z8aRjaIbXhI1G+NtaNY7kSRrE4
# fAyC2X2zV5i4a0AuAMM40C1Wm3gTaNtRTHnka/pbynUlFjP+KqAZhOniJg4AUfjX
# sG+PG1LH2+w/sfDl1A8liXSZU1qJtUs3wBQFoSGEaGBeDQIDAQABo4ICJTCCAiEw
# HwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFIhC+HL9
# QlvsWsztP/I5wYwdfCFNMB0GA1UdEQQWMBSBEm5vcmVwbHlAdm13YXJlLmNvbTAO
# BgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCB
# qjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3Rl
# ZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0
# dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu
# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQB
# MCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYI
# KwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0
# LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEACQAYaQI6Nt2KgxdN
# 6qqfcHB33EZRSXkvs8O9iPZkdDjEx+2fgbBPLUvk9A7T8mRw7brbcJv4PLTYJDFo
# c5mlcmG7/5zwTOuIs2nBGXc/uxCnyW8p7kD4Y0JxPKEVQoIQ8lJS9Uy/hBjyakeV
# ef982JyzvDbOlLBy6AS3ZpXVkRY5y3Va+3v0R/0xJ+JRxUicQhiZRidq2TCiWEas
# d+tLL6jrKaBO+rmP52IM4eS9d4Yids7ogKEBAlJi0NbvuKO0CkgOlFjp1tOvD4sQ
# taHIMmqi40p4Tjyf/sY6yGjROXbMeeF1vlwbBAASPWpQuEIxrNHoVN30YfJyuOWj
# zdiJUTpeLn9XdjM3UlhfaHP+oIAKcmkd33c40SFRlQG9+P9Wlm7TcPxGU4wzXI8n
# Cw/h235jFlAAiWq9L2r7Un7YduqsheJVpGoXmRXJH0T2G2eNFS5/+2sLn98kN2Cn
# J7j6C242onjkZuGL2/+gqx8m5Jbpu9P4IAeTC1He/mX9j6XpIu+7uBoRVwuWD1i0
# N5SiUz7Lfnbr6Q1tHMXKDLFdwVKZos2AKEZhv4SU0WvenMJKDgkkhVeHPHbTahQf
# P1MetR8tdRs7uyTWAjPK5xf5DLEkXbMrUkpJ089fPvAGVHBcHRMqFA5egexOb6sj
# tKncUjJ1xAAtAExGdCh6VD2U5iYxghoDMIIZ/wIBATB9MGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEA7G
# 8rJ2oUagfQ5tk1e14QgwDQYJYIZIAWUDBAIBBQCggZYwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwKgYKKwYB
# BAGCNwIBDDEcMBqhGIAWaHR0cDovL3d3dy52bXdhcmUuY29tLzAvBgkqhkiG9w0B
# CQQxIgQgQ1SATk3LEqVYSVa4pSG7x3SH4yz1E3MjaKOug8ovJ5wwDQYJKoZIhvcN
# AQEBBQAEggGAvvW8iPhGTLAGLrWhGpEbPhOWuolfnC0Kep2ZuEmcRsX2NDI+ZL3j
# wuo+7rd/rwOBZ1ELmHkrXQPsXo7W54qltXoSzG0V8DRC5qIx9TXaO5oI5AHjk+qN
# mit3RBuJlL9hoMd7AIqKTdTin9XAtj8GFcfoa6qkQQqqVV9bnTuApBalwOqO2BgD
# 1PY4IUlC+tDy2b28R6bKHVmiT5S5Q+HqqSX/QweDLsmU8wBI4ZB4LqnpU1ZNvtRs
# bJbecFGMYGGJ0aW4OIap9M6S+wZf9agvoj2WPobeCqtMMbdJXdBvVSVvQBrV2fBg
# wCAXlTk4kuTRGq7bg7cZFCCejcoCW6fAPw7IEN704ekF5OjaHXEjcqOYs2+iktdg
# qygftPAmfMtl91HuNcmkVXQkGF9WsFTd/NEt1OhbHShOdUcoCObMJVruo0gjgEVv
# ovFkRlOTbt9C7O5sva7tjG1Mzv8Szki9Ix7tmMugk9QC9fxfPm+/541tA+SlJ2B1
# gXAKPkHwmZS8oYIXPjCCFzoGCisGAQQBgjcDAwExghcqMIIXJgYJKoZIhvcNAQcC
# oIIXFzCCFxMCAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBKBpBGcw
# ZQIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIKjicO2YVUOavqmZyFAy
# 8GNJS7OOs18uY4J5kRbEAhqbAhEAnMGNPG4Y2uji9bnJp6jFSxgPMjAyMjEwMTMx
# MzM1MzBaoIITBzCCBsAwggSooAMCAQICEAxNaXJLlPo8Kko9KQeAPVowDQYJKoZI
# hvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQTAeFw0yMjA5MjEwMDAwMDBaFw0zMzExMjEyMzU5NTlaMEYx
# CzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDEkMCIGA1UEAxMbRGlnaUNl
# cnQgVGltZXN0YW1wIDIwMjIgLSAyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAz+ylJjrGqfJru43BDZrboegUhXQzGias0BxVHh42bbySVQxh9J0Jdz0V
# lggva2Sk/QaDFteRkjgcMQKW+3KxlzpVrzPsYYrppijbkGNcvYlT4DotjIdCriak
# 5Lt4eLl6FuFWxsC6ZFO7KhbnUEi7iGkMiMbxvuAvfTuxylONQIMe58tySSgeTIAe
# hVbnhe3yYbyqOgd99qtu5Wbd4lz1L+2N1E2VhGjjgMtqedHSEJFGKes+JvK0jM1M
# uWbIu6pQOA3ljJRdGVq/9XtAbm8WqJqclUeGhXk+DF5mjBoKJL6cqtKctvdPbnjE
# KD+jHA9QBje6CNk1prUe2nhYHTno+EyREJZ+TeHdwq2lfvgtGx/sK0YYoxn2Off1
# wU9xLokDEaJLu5i/+k/kezbvBkTkVf826uV8MefzwlLE5hZ7Wn6lJXPbwGqZIS1j
# 5Vn1TS+QHye30qsU5Thmh1EIa/tTQznQZPpWz+D0CuYUbWR4u5j9lMNzIfMvwi4g
# 14Gs0/EH1OG92V1LbjGUKYvmQaRllMBY5eUuKZCmt2Fk+tkgbBhRYLqmgQ8JJVPx
# vzvpqwcOagc5YhnJ1oV/E9mNec9ixezhe7nMZxMHmsF47caIyLBuMnnHC1mDjcbu
# 9Sx8e47LZInxscS451NeX1XSfRkpWQNO+l3qRXMchH7XzuLUOncCAwEAAaOCAYsw
# ggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoG
# CCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAfBgNV
# HSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQUYore0GH8jzEU
# 7ZcLzT0qlBTfUpwwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGlnaWNl
# cnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGlu
# Z0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFt
# cGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAVaoqGvNG83hXNzD8deNP1oUj
# 8fz5lTmbJeb3coqYw3fUZPwV+zbCSVEseIhjVQlGOQD8adTKmyn7oz/AyQCbEx2w
# mIncePLNfIXNU52vYuJhZqMUKkWHSphCK1D8G7WeCDAJ+uQt1wmJefkJ5ojOfRu4
# aqKbwVNgCeijuJ3XrR8cuOyYQfD2DoD75P/fnRCn6wC6X0qPGjpStOq/CUkVNTZZ
# mg9U0rIbf35eCa12VIp0bcrSBWcrduv/mLImlTgZiEQU5QpZomvnIj5EIdI/HMCb
# 7XxIstiSDJFPPGaUr10CU+ue4p7k0x+GAWScAMLpWnR1DT3heYi/HAGXyRkjgNc2
# Wl+WFrFjDMZGQDvOXTXUWT5Dmhiuw8nLw/ubE19qtcfg8wXDWd8nYiveQclTuf80
# EGf2JjKYe/5cQpSBlIKdrAqLxksVStOYkEVgM4DgI974A6T2RUflzrgDQkfoQTZx
# d639ouiXdE4u2h4djFrIHprVwvDGIqhPm73YHJpRxC+a9l+nJ5e6li6FV8Bg53hW
# f2rvwpWaSxECyIKcyRoFfLpxtU56mWz06J7UWpjIn7+NuxhcQ/XQKujiYu54BNu9
# 0ftbCqhwfvCXhHjjCANdRyxjqCU4lwHSPzra5eX25pvcfizM/xdMTQCi2NYBDriL
# 7ubgclWJLCcZYfZ3AYwwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0G
# CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTla
# MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UE
# AxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBp
# bmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJ
# UVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+e
# DzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47q
# UT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL
# 6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c
# 1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052
# FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+
# onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/w
# ojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1
# eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uK
# IqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7p
# XcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgw
# BgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgw
# FoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQM
# MAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDov
# L29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6
# MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# Um9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJ
# KoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7
# x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGId
# DAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7g
# iqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6
# wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx
# 2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5kn
# LD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3it
# TK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7
# HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUV
# mDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKm
# KYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8
# MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwFADBl
# MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
# d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
# b3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G
# CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7J
# IT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxS
# D1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb
# 7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1ef
# VFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoY
# OAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSa
# M0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI
# 8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9L
# BADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfm
# Q6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDr
# McXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15Gkv
# mB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
# FgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGL
# p6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggrBgEF
# BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRw
# Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu
# Y3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAAMA0G
# CSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzshV6p
# Grsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre+i1W
# z/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8vC6bp
# 8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38dglo
# hJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNrIv8S
# uFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEwdzBj
# MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMT
# MkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5n
# IENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqGSIb3
# DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjIxMDEzMTMzNTMw
# WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTzhyJNhjOCkjWplLy9j5bp/hx8czAv
# BgkqhkiG9w0BCQQxIgQg9J9wcL+p1DfkaFJp7e+2xWQzxfLjbB6CVH6yHajdHv0w
# NwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgx/ThvjIoiSCr4iY6vhrE/E/meBwtZNBM
# gHVXoCO1tvowDQYJKoZIhvcNAQEBBQAEggIAYk4uKnTCNEgtpwviYm3hYel3KY4J
# LdFQ7LIa/gsEAJHkbiIqiJfOdMNL/plaA7BIWhs0sPR6D1spXWzlp/wRZHg+/btw
# LOzoWQSgOS6OINFSqOdmQ4/TwsSLOLYJOEWF2mZd71fT5+J/1MAvu0DSzyVdi3eI
# 8/wpQ4vydbKWajGD7xHZ+0L4nVKGpn+EAwKUqzPrG0YGi6qCfJRcmPTzSBiEpRpX
# aSxgugqkMtqY0ANJZ51qW7ePDMyPJ8BPxd9GzHzIR5HXaKNtQK2iy2xWwcORsbIJ
# 18XXDBAFhsxJ6N7QJgOhIj+H32MQWAfOwxa43uxHtnTZClmN2kG057IPcA8XDJwf
# K9JdsVxnQ+L6Gbvqw2bfPUlVMePjSl+MIxOjqFdhzKp0ysgqOe+ELzTN8aIqcmAC
# /AsQTcYlURyVU/mKW8phP5RAj6kfwKhlCCrqjIoOgCio+fVPgPrUKi0dP0MRljfS
# vOjPxdWNtZkO07naD3xWF7b9PdhYD7an7Gugc8NOjPADrgqXmebbj1vAGluACkty
# s/Nr6IOpx/pnIpofNUYcdDHhSGv2FaS/ifDBJwSXYJB8Z9bGPgjhSsfNws3dkMv8
# EhxjSNZiqieBauPdjCj/AKSBrCr2JY7ZqpuuZuwhHb3B7BEtXYG5siBp9vlldVpH
# EGBdk1WzOIeOLbE=
# SIG # End signature block