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
# MIIexwYJKoZIhvcNAQcCoIIeuDCCHrQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# 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
# tKncUjJ1xAAtAExGdCh6VD2U5iYxghBDMIIQPwIBATB9MGkxCzAJBgNVBAYTAlVT
# 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
# gXAKPkHwmZS8oYINfjCCDXoGCisGAQQBgjcDAwExgg1qMIINZgYJKoZIhvcNAQcC
# oIINVzCCDVMCAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBKBpBGcw
# ZQIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIKjicO2YVUOavqmZyFAy
# 8GNJS7OOs18uY4J5kRbEAhqbAhEAtlQlWf2Wdu1CFXElRUHcNRgPMjAyMTA5MTMx
# NjA0MDhaoIIKNzCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZI
# hvcNAQELBQAwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
# MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hB
# MiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0z
# MTAxMDYwMDAwMDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjEgMB4GA1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVN
# lLQMULUmEP4dyG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXe
# AhjzeG28ffnHbQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPs
# Tm0zinxdRS1LsVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMD
# gzEpSlwMMYpKjIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8
# H6GHq756WwogL0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4
# MIIBtDAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAK
# BggrBgEFBQcDCDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIB
# FhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+
# Ka7S5GGlsqIlssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEG
# A1UdHwRqMGgwMqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFz
# c3VyZWQtdHMuY3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hh
# Mi1hc3N1cmVkLXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBp
# bmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJR
# dAojR6OrktIlxHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo
# 9VFKeHk2eie38+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPt
# kMyNoRdzDfTzZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ
# 2CKadRVC9S0yOIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdK
# DXF+l7f0P4TrweOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUx
# MIIEGaADAgECAhAKoSXW1jIbfkHkBdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJ
# BgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k
# aWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBD
# QTAeFw0xNjAxMDcxMjAwMDBaFw0zMTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBp
# bmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92p
# uoKZxTlUKFe2I0rEDgdFM1EQfdD5fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I
# 1moUADj3Lh477sym9jJZ/l9lP+Cb6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6
# Ij4YrBHk8JkDbTuFfAnT7l3ImgtU46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkU
# xk0kIzBdvOw8YGqsLwfM/fDqR9mIUF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqY
# D4R/nzEU1q3V8mTLex4F0IQZchfxFwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXd
# rKzpVv+TAgMBAAGjggHOMIIByjAdBgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgX
# NW4wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgw
# BgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYI
# KwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6
# Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmww
# OqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ
# RFJvb3RDQS5jcmwwUAYDVR0gBEkwRzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUH
# AgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0G
# CSqGSIb3DQEBCwUAA4IBAQBxlRLpUYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC
# /cWnww4gQiyvd/MrHwwhWiq3BTQdaq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6C
# JR7Euhx7LCHi1lssFDVDBGiy23UC4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyv
# KYnleB/WCxSlgNcSR3CzddWThZN+tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOF
# IoxhynmUfln8jA/jb7UBJrZspe6HUSHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bh
# fEJCKMYYVs9BNLZmXbZ0e/VWMyIvIjayS6JKldj1po5SMYIChjCCAoICAQEwgYYw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIFRpbWVzdGFtcGluZyBDQQIQDUJK4L46iP9gQCHOFADw3TANBglghkgBZQME
# AgEFAKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkF
# MQ8XDTIxMDkxMzE2MDQwOFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU4deCqOGR
# vu9ryhaRtaq0lKYkm/MwLwYJKoZIhvcNAQkEMSIEIC8DsxfnhAT4yV2pC4z00LkT
# CdwhzDFjIk9SdL5eG1tsMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEILMQkAa8CtmD
# B5FXKeBEA0Fcg+MpK2FPJpZMjTVx7PWpMA0GCSqGSIb3DQEBAQUABIIBAEfp4Vqj
# KelDmnsLyhJaSG696/ff3nd57bFa0iz0WFgZc3BDNYmREy2IQJcJFnnHEkadz9h1
# 3tq2qX3DDIOgmvA/unVIfKuRQ//3iFANpvtkA3HPs/D/SRD6gNXPMScEKamU3xcG
# EmPyhI8/ijPuP0R3s8KSQJueXGEKZetVfshZcvqBOSIbdOqGHa+I5GrLI2MSCA8H
# NdKgLwWaSmvAwQ3mkbQ9BwCSLy9Yx0YPpUR69tuUmpwYt3gx6w6gdt9g7bn9hLrT
# LIOktk40Czq4Ob0QXcHX2a3MIVCh9PXm20gaeuG64aHixv5yl2VhKByqKOCRl0Md
# a0li9WPewyNi7e4=
# SIG # End signature block