helpers/test/Invoke-WUGGeomapTest.ps1
|
<#
.SYNOPSIS End-to-end integration test for WhatsUpGoldPS geolocation map helpers. .DESCRIPTION Connects to a WhatsUp Gold server, creates test devices and groups with latitude/longitude data, exercises the geolocation helper functions (Get-GeoDevicesWithLocation, Get-GeoGroupsWithLocation, Get-GeolocationData, Export-GeolocationMapHtml), and cleans up all test artefacts. Tests both the WhatsUpGoldPS module (attribute and group APIs) and the standalone GeolocationHelpers functions. .PARAMETER ServerUri The WhatsUp Gold server hostname or IP. If omitted you will be prompted. .PARAMETER Credential A PSCredential for authentication. If omitted you will be prompted. .PARAMETER Port API port. Default 9644. .PARAMETER Protocol http or https. Default https. .PARAMETER IgnoreSSLErrors Pass -IgnoreSSLErrors to Connect-WUGServer when set. .EXAMPLE .\Invoke-WUGGeomapTest.ps1 .EXAMPLE .\Invoke-WUGGeomapTest.ps1 -ServerUri "192.168.74.74" -Credential (Get-Credential) .NOTES Author : Jason Alberino (jason@wug.ninja) Created: 2025-07-15 Requires: WhatsUpGoldPS module loaded, GeolocationHelpers.ps1 in ../geolocation/ #> [CmdletBinding()] param( [string]$ServerUri, [PSCredential]$Credential, [int]$Port = 9644, [ValidateSet('http', 'https')] [string]$Protocol = 'https', [switch]$IgnoreSSLErrors ) #region -- Helpers ------------------------------------------------------------ $script:TestResults = [System.Collections.Generic.List[PSCustomObject]]::new() function Record-Test { param( [string]$Cmdlet, [string]$Endpoint, [string]$Status, [string]$Detail = '' ) $script:TestResults.Add([PSCustomObject]@{ Cmdlet = $Cmdlet Endpoint = $Endpoint Status = $Status Detail = $Detail }) $color = switch ($Status) { 'Pass' { 'Green' } 'Fail' { 'Red' } default { 'Yellow' } } Write-Host " [$Status] $Cmdlet ($Endpoint) $Detail" -ForegroundColor $color } function Invoke-Test { param( [string]$Cmdlet, [string]$Endpoint, [scriptblock]$Test ) try { $null = & $Test Record-Test -Cmdlet $Cmdlet -Endpoint $Endpoint -Status 'Pass' } catch { Record-Test -Cmdlet $Cmdlet -Endpoint $Endpoint -Status 'Fail' -Detail $_.Exception.Message } } #endregion #region -- Module + helper imports -------------------------------------------- if (-not (Get-Module -Name WhatsUpGoldPS)) { $modulePath = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'WhatsUpGoldPS.psm1' try { Import-Module $modulePath -Force -ErrorAction Stop } catch { Write-Error "Cannot load WhatsUpGoldPS module: $_"; return } } $geoHelpersPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\GeolocationHelpers.ps1' if (-not (Test-Path $geoHelpersPath)) { Write-Error "GeolocationHelpers.ps1 not found at: $geoHelpersPath"; return } . $geoHelpersPath # Load vault functions for credential resolution $discoveryHelpersPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'discovery\DiscoveryHelpers.ps1' if (Test-Path $discoveryHelpersPath) { . $discoveryHelpersPath } #endregion #region -- Prompt for connection details -------------------------------------- if (-not $ServerUri) { $wugCred = Resolve-DiscoveryCredential -Name 'WUG.Server' -CredType WUGServer -ProviderLabel 'WhatsUp Gold' -AutoUse if ($wugCred) { $ServerUri = $wugCred.Server if (-not $Credential) { $Credential = $wugCred.Credential } } else { $ServerUri = Read-Host "Enter WhatsUp Gold server hostname or IP" } } if (-not $Credential) { $Credential = Get-Credential -Message "Enter WhatsUp Gold credentials" } #endregion #region -- Connect (WhatsUpGoldPS module) ------------------------------------- Write-Host "`n============================================================" -ForegroundColor Cyan Write-Host " WhatsUpGoldPS Geolocation Map E2E Test Suite" -ForegroundColor Cyan Write-Host "============================================================`n" -ForegroundColor Cyan # Clear any stale geolocation cache to ensure clean test state $geoCacheDir = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Cache' foreach ($f in @('geolocation-coords.json', 'geolocation-no-coords.json')) { $p = Join-Path $geoCacheDir $f if (Test-Path $p) { Remove-Item $p -Force -ErrorAction SilentlyContinue } } Write-Host " Cleared geolocation cache files" -ForegroundColor DarkGray $connectParams = @{ serverUri = $ServerUri Credential = $Credential Port = $Port Protocol = $Protocol IgnoreSSLErrors = $true } Write-Host "[1/8] Connecting to $ServerUri via WhatsUpGoldPS ..." -ForegroundColor Cyan Invoke-Test -Cmdlet 'Connect-WUGServer' -Endpoint 'POST /token' -Test { Connect-WUGServer @connectParams -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 $script:TestResults | Format-Table -AutoSize -Property Cmdlet, Endpoint, Status, Detail return } #endregion #region -- Connect (Geolocation Helpers - independent auth) ------------------- Write-Host "`n[2/8] Connecting via GeolocationHelpers Connect-GeoWUGServer ..." -ForegroundColor Cyan $script:GeoConfig = $null Invoke-Test -Cmdlet 'Connect-GeoWUGServer' -Endpoint 'POST /token (geo)' -Test { $username = $Credential.GetNetworkCredential().UserName $password = $Credential.GetNetworkCredential().Password $script:GeoConfig = Connect-GeoWUGServer -ServerUri $ServerUri -Username $username -Password $password ` -Protocol $Protocol -Port $Port -IgnoreSSLErrors -ErrorAction Stop $password = $null if (-not $script:GeoConfig._AccessToken) { throw "No access token returned" } } if (-not $script:GeoConfig) { Write-Host "`n FATAL: Geo helper authentication failed. Cannot continue geo tests." -ForegroundColor Red $script:TestResults | Format-Table -AutoSize -Property Cmdlet, Endpoint, Status, Detail return } #endregion #region -- Create test artefacts with geolocation data ------------------------ Write-Host "`n[3/8] Creating test device and group with lat/lng data ..." -ForegroundColor Cyan $script:GeoTestDeviceId = $null $script:GeoTestGroupId = $null $script:GeoTestDeviceName = "WUGPS-GeoTest-$([guid]::NewGuid().ToString('N').Substring(0,8))" $script:GeoTestGroupName = "WUGPS-GeoGroup-$([guid]::NewGuid().ToString('N').Substring(0,8))" # Test latitude/longitude values (New York City) $testLat = '40.7128' $testLng = '-74.0060' # -- Create a test device ----------------------------------------------------- Invoke-Test -Cmdlet 'Add-WUGDeviceTemplate (geo device)' -Endpoint 'POST /devices/-/config/template' -Test { $result = Add-WUGDeviceTemplate -DeviceAddress '127.0.0.5' -displayName $script:GeoTestDeviceName ` -primaryRole 'Device' -note "Geolocation test device" -Confirm:$false -ErrorAction Stop if (-not $result) { throw "No result" } $script:GeoTestDeviceId = $result.idMap.resultId if (-not $script:GeoTestDeviceId) { throw "No resultId" } } if ($script:GeoTestDeviceId) { Start-Sleep -Seconds 3 # -- Set LatLong attribute (single "lat,lng" format) ---------------------- Invoke-Test -Cmdlet 'Set-WUGDeviceAttribute (LatLong)' -Endpoint 'POST /devices/{id}/attributes/-' -Test { Set-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Name "LatLong" -Value "$testLat,$testLng" ` -Confirm:$false -ErrorAction Stop | Out-Null } # -- Verify attribute was set --------------------------------------------- Invoke-Test -Cmdlet 'Get-WUGDeviceAttribute (LatLong verify)' -Endpoint 'GET /devices/{id}/attributes/-' -Test { $attrs = Get-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Names "LatLong" -ErrorAction Stop $found = $false if ($attrs) { foreach ($a in @($attrs)) { if ($a.name -eq 'LatLong' -and $a.value -match "$testLat") { $found = $true; break } } } if (-not $found) { throw "LatLong attribute not found or value mismatch" } } # -- Also test separate Latitude/Longitude attributes --------------------- Invoke-Test -Cmdlet 'Set-WUGDeviceAttribute (Latitude)' -Endpoint 'POST /devices/{id}/attributes/-' -Test { Set-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Name "Latitude" -Value $testLat ` -Confirm:$false -ErrorAction Stop | Out-Null } Invoke-Test -Cmdlet 'Set-WUGDeviceAttribute (Longitude)' -Endpoint 'POST /devices/{id}/attributes/-' -Test { Set-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Name "Longitude" -Value $testLng ` -Confirm:$false -ErrorAction Stop | Out-Null } } # -- Create a test group with lat,lng in description ------------------------- Invoke-Test -Cmdlet 'Add-WUGDeviceGroup (geo group)' -Endpoint 'POST /device-groups/{id}/children' -Test { $result = Add-WUGDeviceGroup -ParentGroupId 0 -Name $script:GeoTestGroupName ` -Description "$testLat,$testLng" -Confirm:$false -ErrorAction Stop if (-not $result) { throw "No result from Add-WUGDeviceGroup" } $script:GeoTestGroupId = if ($result.id) { $result.id } elseif ($result.groupId) { $result.groupId } else { $result } } #endregion #region -- Test GeolocationHelpers functions ---------------------------------- Write-Host "`n[4a/8] Testing GeolocationHelpers data-retrieval functions ..." -ForegroundColor Cyan $script:GeoDeviceData = $null $script:GeoGroupData = $null $script:GeoAllData = $null # Wait for the test device to appear in the root group device list. # WUG may take a few seconds to index a newly created device. if ($script:GeoTestDeviceId -and $script:GeoConfig) { $maxWaitSec = 30; $waitedSec = 0 $deviceInList = $false while (-not $deviceInList -and $waitedSec -lt $maxWaitSec) { try { $listResult = Invoke-RestMethod -Uri "$($script:GeoConfig.BaseUri)/api/v1/device-groups/0/devices/-?view=overview&limit=500" ` -Headers @{ "Content-Type" = "application/json"; "Authorization" = "$($script:GeoConfig._TokenType) $($script:GeoConfig._AccessToken)" } -ErrorAction Stop if ($listResult.data.devices) { foreach ($d in $listResult.data.devices) { $dId = if ($d.id) { $d.id } else { $d.deviceId } if ("$dId" -eq "$($script:GeoTestDeviceId)") { $deviceInList = $true; break } } } } catch { Write-Verbose "Device list check error: $_" } if (-not $deviceInList) { Start-Sleep -Seconds 5; $waitedSec += 5 } } if ($deviceInList) { Write-Host " Test device visible in root group (waited ${waitedSec}s)" -ForegroundColor DarkGray } else { Write-Warning "Test device not in root group after ${maxWaitSec}s - scan tests may fail" } } # -- Get-GeoDevicesWithLocation (LatLong attribute) --------------------------- if ($script:GeoTestDeviceId -and $script:GeoConfig) { Invoke-Test -Cmdlet 'Get-GeoDevicesWithLocation (LatLong)' -Endpoint 'GET /devices/-/attributes (LatLong)' -Test { $result = Get-GeoDevicesWithLocation -Config $script:GeoConfig -ErrorAction Stop $script:GeoDeviceData = $result # Look for our test device in the results $found = $false if ($result) { foreach ($d in @($result)) { if ($d.Name -eq $script:GeoTestDeviceName) { $found = $true; break } } } if (-not $found) { throw "Test device '$($script:GeoTestDeviceName)' not found in geolocation results" } } # -- Get-GeoDevicesWithLocation (UseBuiltinCoords) ------------------------ Invoke-Test -Cmdlet 'Get-GeoDevicesWithLocation (BuiltinCoords)' -Endpoint 'GET /devices/-/attributes (Lat+Lng)' -Test { $result = Get-GeoDevicesWithLocation -Config $script:GeoConfig -UseBuiltinCoords -ErrorAction Stop $found = $false if ($result) { foreach ($d in @($result)) { if ($d.Name -eq $script:GeoTestDeviceName) { if ($d.Latitude -ne [double]$testLat -or $d.Longitude -ne [double]$testLng) { throw "Coordinates mismatch: expected $testLat,$testLng got $($d.Latitude),$($d.Longitude)" } $found = $true; break } } } if (-not $found) { throw "Test device not found with builtin coords" } } } # -- Get-GeoGroupsWithLocation ----------------------------------------------- if ($script:GeoTestGroupId -and $script:GeoConfig) { Invoke-Test -Cmdlet 'Get-GeoGroupsWithLocation' -Endpoint 'GET /device-groups/-/definition' -Test { $result = Get-GeoGroupsWithLocation -Config $script:GeoConfig -ErrorAction Stop $script:GeoGroupData = $result $found = $false if ($result) { foreach ($g in @($result)) { if ($g.Name -eq $script:GeoTestGroupName) { if ($g.Latitude -ne [double]$testLat -or $g.Longitude -ne [double]$testLng) { throw "Group coordinates mismatch: expected $testLat,$testLng got $($g.Latitude),$($g.Longitude)" } $found = $true; break } } } if (-not $found) { throw "Test group '$($script:GeoTestGroupName)' not found in geolocation results" } } } # -- Get-GeolocationData (combined) ------------------------------------------ if ($script:GeoConfig) { Invoke-Test -Cmdlet 'Get-GeolocationData (all)' -Endpoint 'GET /devices + /device-groups (combined)' -Test { $result = Get-GeolocationData -Config $script:GeoConfig -ErrorAction Stop $script:GeoAllData = $result if ($null -eq $result -or @($result).Count -eq 0) { throw "No geolocation data returned" } } Invoke-Test -Cmdlet 'Get-GeolocationData (devices only)' -Endpoint 'GET /devices (devices only)' -Test { $result = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $true -IncludeGroups $false -ErrorAction Stop if ($result) { $hasGroup = $false foreach ($r in @($result)) { if ($r.Type -eq 'Group') { $hasGroup = $true; break } } if ($hasGroup) { throw "Groups should be excluded but were found" } } } Invoke-Test -Cmdlet 'Get-GeolocationData (groups only)' -Endpoint 'GET /device-groups (groups only)' -Test { $result = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $false -IncludeGroups $true -ErrorAction Stop if ($result) { $hasDevice = $false foreach ($r in @($result)) { if ($r.Type -eq 'Device') { $hasDevice = $true; break } } if ($hasDevice) { throw "Devices should be excluded but were found" } } } } #endregion #region -- Test new geolocation parameters ------------------------------------ Write-Host "`n[4b/8] Testing new geolocation parameters (cache, skip, refresh) ..." -ForegroundColor Cyan # -- Get-GeoDevicesWithLocation -RefreshCache --------------------------------- if ($script:GeoTestDeviceId -and $script:GeoConfig) { Invoke-Test -Cmdlet 'Get-GeoDevicesWithLocation (-RefreshCache)' -Endpoint 'GET /devices/-/attributes (force refresh)' -Test { $result = Get-GeoDevicesWithLocation -Config $script:GeoConfig -RefreshCache -ErrorAction Stop $found = $false if ($result) { foreach ($d in @($result)) { if ($d.Name -eq $script:GeoTestDeviceName) { $found = $true; break } } } if (-not $found) { throw "Test device not found after RefreshCache" } } # Verify cache file was created Invoke-Test -Cmdlet 'Location cache file exists' -Endpoint '(cache file check)' -Test { $cacheFile = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Cache\geolocation-coords.json' if (-not (Test-Path $cacheFile)) { throw "Cache file not created at: $cacheFile" } $cacheContent = Get-Content $cacheFile -Raw -Encoding UTF8 | ConvertFrom-Json if (-not $cacheContent -or @($cacheContent).Count -eq 0) { throw "Cache file is empty" } } # Run again WITHOUT -RefreshCache to exercise cache-hit path Invoke-Test -Cmdlet 'Get-GeoDevicesWithLocation (cache hit)' -Endpoint 'GET /devices/-/attributes (cached)' -Test { $result = Get-GeoDevicesWithLocation -Config $script:GeoConfig -ErrorAction Stop $found = $false if ($result) { foreach ($d in @($result)) { if ($d.Name -eq $script:GeoTestDeviceName) { $found = $true; break } } } if (-not $found) { throw "Test device not found on cached run" } } } # -- Get-GeolocationData with SkipDevices / SkipGroups equivalents ------------ if ($script:GeoConfig) { # IncludeDevices=$false is the equivalent of SkipDevices on the helper level Invoke-Test -Cmdlet 'Get-GeolocationData (skip devices)' -Endpoint 'GET /device-groups only (skip devices)' -Test { $result = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $false -IncludeGroups $true -ErrorAction Stop if ($result) { $hasDevice = $false foreach ($r in @($result)) { if ($r.Type -eq 'Device') { $hasDevice = $true; break } } if ($hasDevice) { throw "Devices should be excluded with SkipDevices but were found" } } } Invoke-Test -Cmdlet 'Get-GeolocationData (skip groups)' -Endpoint 'GET /devices only (skip groups)' -Test { $result = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $true -IncludeGroups $false -ErrorAction Stop if ($result) { $hasGroup = $false foreach ($r in @($result)) { if ($r.Type -eq 'Group') { $hasGroup = $true; break } } if ($hasGroup) { throw "Groups should be excluded with SkipGroups but were found" } } } } # -- Export-GeolocationMapHtml with -RefreshIntervalSeconds ------------------- if ($script:GeoAllData -and @($script:GeoAllData).Count -gt 0) { $script:RefreshMapPath = Join-Path $env:TEMP "WUGPS-GeoTest-Refresh-$(Get-Date -Format 'yyyyMMddHHmmss').html" Invoke-Test -Cmdlet 'Export-GeolocationMapHtml (-RefreshIntervalSeconds)' -Endpoint '(HTML auto-refresh)' -Test { $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($script:GeoAllData) -OutputPath $script:RefreshMapPath ` -TemplatePath $templatePath -RefreshIntervalSeconds 300 -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "Refresh map file not created" } $content = Get-Content $resultPath -Raw if ($content -notmatch 'meta http-equiv="refresh" content="300"') { throw "Auto-refresh meta tag not found in generated HTML" } } # Verify that RefreshIntervalSeconds=0 does NOT inject the tag Invoke-Test -Cmdlet 'Export-GeolocationMapHtml (no refresh)' -Endpoint '(HTML no auto-refresh)' -Test { $noRefreshPath = Join-Path $env:TEMP "WUGPS-GeoTest-NoRefresh-$(Get-Date -Format 'yyyyMMddHHmmss').html" $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($script:GeoAllData) -OutputPath $noRefreshPath ` -TemplatePath $templatePath -RefreshIntervalSeconds 0 -ErrorAction Stop $content = Get-Content $resultPath -Raw if ($content -match 'http-equiv="refresh"') { throw "Auto-refresh meta tag should NOT be present when interval is 0" } Remove-Item $noRefreshPath -Force -ErrorAction SilentlyContinue } } #endregion #region -- Test Set-GeoDeviceLocations (CSV sync) ----------------------------- Write-Host "`n[4c/8] Testing Set-GeoDeviceLocations (CSV sync) ..." -ForegroundColor Cyan if ($script:GeoTestDeviceId -and $script:GeoConfig) { # Get the test device's display name for CSV matching $script:SyncCsvPath = Join-Path $env:TEMP "WUGPS-GeoTest-Sync-$(Get-Date -Format 'yyyyMMddHHmmss').csv" $script:SyncCsvBadPath = Join-Path $env:TEMP "WUGPS-GeoTest-SyncBad-$(Get-Date -Format 'yyyyMMddHHmmss').csv" # -- Create a valid CSV with the test device and a non-existent device ----- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (create CSV)' -Endpoint '(CSV creation)' -Test { $csvContent = @" DeviceName,Latitude,Longitude $($script:GeoTestDeviceName),34.0522,-118.2437 NonExistentDevice-WUGPS-99999,51.5074,-0.1278 "@ Set-Content -Path $script:SyncCsvPath -Value $csvContent -Encoding UTF8 if (-not (Test-Path $script:SyncCsvPath)) { throw "CSV file not created" } } # -- Test sync with valid CSV (default LatLong mode) ---------------------- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (sync LatLong)' -Endpoint 'PATCH /devices/{id}/attributes/-' -Test { $results = Set-GeoDeviceLocations -Config $script:GeoConfig -CsvPath $script:SyncCsvPath -ErrorAction Stop if (-not $results -or @($results).Count -eq 0) { throw "No results returned" } $updated = @($results | Where-Object { $_.Status -eq 'Updated' }) $notFound = @($results | Where-Object { $_.Status -eq 'NotFound' }) if ($updated.Count -eq 0) { throw "Expected at least 1 Updated device, got 0" } if ($notFound.Count -eq 0) { throw "Expected at least 1 NotFound device, got 0" } } # -- Verify the attribute was actually updated on the device --------------- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (verify attribute)' -Endpoint 'GET /devices/{id}/attributes/-' -Test { $attrs = Get-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Names "LatLong" -ErrorAction Stop $found = $false if ($attrs) { foreach ($a in @($attrs)) { if ($a.name -eq 'LatLong' -and $a.value -match '34\.0522') { $found = $true; break } } } if (-not $found) { throw "LatLong attribute not updated to CSV value (expected 34.0522)" } } # -- Test sync with UseSeparateAttributes --------------------------------- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (separate attrs)' -Endpoint 'PATCH /devices/{id}/attributes/- (Lat+Lng)' -Test { $results = Set-GeoDeviceLocations -Config $script:GeoConfig -CsvPath $script:SyncCsvPath ` -UseSeparateAttributes -ErrorAction Stop $updated = @($results | Where-Object { $_.Status -eq 'Updated' }) if ($updated.Count -eq 0) { throw "Expected at least 1 Updated device with separate attrs" } } # -- Verify separate attributes were written ------------------------------ Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (verify Lat+Lng)' -Endpoint 'GET /devices/{id}/attributes/- (Lat+Lng)' -Test { $attrs = Get-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Names "Latitude","Longitude" -ErrorAction Stop $latOk = $false; $lngOk = $false if ($attrs) { foreach ($a in @($attrs)) { if ($a.name -eq 'Latitude' -and $a.value -match '34\.0522') { $latOk = $true } if ($a.name -eq 'Longitude' -and $a.value -match '-118\.2437') { $lngOk = $true } } } if (-not $latOk) { throw "Latitude attribute not set correctly" } if (-not $lngOk) { throw "Longitude attribute not set correctly" } } # -- Test WhatIf mode (should not change anything) ------------------------ Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (-WhatIf)' -Endpoint '(dry run)' -Test { # First reset LatLong back to known value Set-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Name "LatLong" -Value "$testLat,$testLng" ` -Confirm:$false -ErrorAction Stop | Out-Null # Create CSV with different coords $whatIfCsv = Join-Path $env:TEMP "WUGPS-GeoTest-WhatIf-$(Get-Date -Format 'yyyyMMddHHmmss').csv" Set-Content -Path $whatIfCsv -Value "DeviceName,Latitude,Longitude`n$($script:GeoTestDeviceName),45.0,45.0" -Encoding UTF8 $results = Set-GeoDeviceLocations -Config $script:GeoConfig -CsvPath $whatIfCsv -WhatIf -ErrorAction Stop $whatIfResults = @($results | Where-Object { $_.Status -eq 'WhatIf' }) if ($whatIfResults.Count -eq 0) { throw "Expected WhatIf status in results" } # Verify attribute was NOT changed $attrs = Get-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Names "LatLong" -ErrorAction Stop foreach ($a in @($attrs)) { if ($a.name -eq 'LatLong' -and $a.value -match '45\.0') { throw "WhatIf should not have changed the attribute" } } Remove-Item $whatIfCsv -Force -ErrorAction SilentlyContinue } # -- Test invalid CSV (bad coordinates) ----------------------------------- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (bad coords)' -Endpoint '(validation)' -Test { $csvContent = @" DeviceName,Latitude,Longitude $($script:GeoTestDeviceName),999,-999 "@ Set-Content -Path $script:SyncCsvBadPath -Value $csvContent -Encoding UTF8 $results = Set-GeoDeviceLocations -Config $script:GeoConfig -CsvPath $script:SyncCsvBadPath -ErrorAction Stop $skipped = @($results | Where-Object { $_.Status -eq 'Skipped' }) if ($skipped.Count -eq 0) { throw "Expected Skipped status for out-of-range coordinates" } } # -- Test CSV with IP column fallback ------------------------------------- Invoke-Test -Cmdlet 'Set-GeoDeviceLocations (IP fallback)' -Endpoint 'PATCH /devices/{id}/attributes/- (IP match)' -Test { $csvContent = @" DeviceName,IP,Latitude,Longitude SomeUnknownName-WUGPS,127.0.0.5,48.8566,2.3522 "@ $ipCsv = Join-Path $env:TEMP "WUGPS-GeoTest-IP-$(Get-Date -Format 'yyyyMMddHHmmss').csv" Set-Content -Path $ipCsv -Value $csvContent -Encoding UTF8 $results = Set-GeoDeviceLocations -Config $script:GeoConfig -CsvPath $ipCsv -ErrorAction Stop # The test device is at 127.0.0.5 so it should match via IP $updated = @($results | Where-Object { $_.Status -eq 'Updated' }) if ($updated.Count -eq 0) { throw "Expected IP-fallback match to update device" } Remove-Item $ipCsv -Force -ErrorAction SilentlyContinue } # Restore LatLong to original test value for subsequent tests Set-WUGDeviceAttribute -DeviceId $script:GeoTestDeviceId -Name "LatLong" -Value "$testLat,$testLng" ` -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } else { Record-Test -Cmdlet 'Set-GeoDeviceLocations' -Endpoint '(skipped)' -Status 'Skipped' -Detail 'No test device or geo config' } #endregion #region -- Test Update-GeolocationMap (Export-GeolocationMapHtml + Get-GeolocationData) --- Write-Host "`n[4d/8] Testing Update-GeolocationMap pipeline (Get-GeolocationData + Export) ..." -ForegroundColor Cyan if ($script:GeoConfig) { # -- Full pipeline: Get-GeolocationData -> Export-GeolocationMapHtml ------ $script:UpdateMapPath = Join-Path $env:TEMP "WUGPS-GeoTest-UpdateMap-$(Get-Date -Format 'yyyyMMddHHmmss').html" Invoke-Test -Cmdlet 'Update-GeolocationMap pipeline (full)' -Endpoint 'GET /devices + /groups -> HTML' -Test { $geoData = Get-GeolocationData -Config $script:GeoConfig -ErrorAction Stop if (-not $geoData -or @($geoData).Count -eq 0) { throw "No geolocation data for map update" } $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($geoData) -OutputPath $script:UpdateMapPath ` -TemplatePath $templatePath -WugBaseUrl "https://$($ServerUri):$Port" -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "Update map file not created" } $content = Get-Content $resultPath -Raw if ($content.Length -lt 100) { throw "Map file too small" } if ($content -notmatch 'leaflet') { throw "Map missing Leaflet references" } } # -- Verify WugBaseUrl is embedded in the output -------------------------- Invoke-Test -Cmdlet 'Update-GeolocationMap (WugBaseUrl)' -Endpoint '(HTML content)' -Test { if (-not (Test-Path $script:UpdateMapPath)) { throw "Map file missing" } $content = Get-Content $script:UpdateMapPath -Raw if ($content -notmatch [regex]::Escape($ServerUri)) { throw "WUG server URI not found in generated map HTML" } } # -- Pipeline with SkipGroups (devices only) ------------------------------ Invoke-Test -Cmdlet 'Update-GeolocationMap (devices only)' -Endpoint 'GET /devices -> HTML (no groups)' -Test { $devOnlyPath = Join-Path $env:TEMP "WUGPS-GeoTest-DevOnly-$(Get-Date -Format 'yyyyMMddHHmmss').html" $geoData = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $true -IncludeGroups $false -ErrorAction Stop if ($geoData) { $hasGroup = $false foreach ($r in @($geoData)) { if ($r.Type -eq 'Group') { $hasGroup = $true; break } } if ($hasGroup) { throw "Groups should be excluded with IncludeGroups=false" } $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($geoData) -OutputPath $devOnlyPath ` -TemplatePath $templatePath -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "Devices-only map not created" } Remove-Item $devOnlyPath -Force -ErrorAction SilentlyContinue } else { throw "No device location data returned for devices-only pipeline" } } # -- Pipeline with SkipDevices (groups only) ------------------------------ Invoke-Test -Cmdlet 'Update-GeolocationMap (groups only)' -Endpoint 'GET /groups -> HTML (no devices)' -Test { $grpOnlyPath = Join-Path $env:TEMP "WUGPS-GeoTest-GrpOnly-$(Get-Date -Format 'yyyyMMddHHmmss').html" $geoData = Get-GeolocationData -Config $script:GeoConfig -IncludeDevices $false -IncludeGroups $true -ErrorAction Stop if ($geoData) { $hasDevice = $false foreach ($r in @($geoData)) { if ($r.Type -eq 'Device') { $hasDevice = $true; break } } if ($hasDevice) { throw "Devices should be excluded with IncludeDevices=false" } } $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($geoData) -OutputPath $grpOnlyPath ` -TemplatePath $templatePath -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "Groups-only map not created" } Remove-Item $grpOnlyPath -Force -ErrorAction SilentlyContinue } # -- Pipeline with auto-refresh (simulates scheduled task output) --------- Invoke-Test -Cmdlet 'Update-GeolocationMap (auto-refresh)' -Endpoint '(HTML scheduled task output)' -Test { $autoRefreshPath = Join-Path $env:TEMP "WUGPS-GeoTest-AutoRefresh-$(Get-Date -Format 'yyyyMMddHHmmss').html" $geoData = Get-GeolocationData -Config $script:GeoConfig -ErrorAction Stop $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $resultPath = Export-GeolocationMapHtml -Data @($geoData) -OutputPath $autoRefreshPath ` -TemplatePath $templatePath -RefreshIntervalSeconds 600 -ErrorAction Stop $content = Get-Content $resultPath -Raw if ($content -notmatch 'meta http-equiv="refresh" content="600"') { throw "Auto-refresh meta tag (600s) not found in scheduled output" } Remove-Item $autoRefreshPath -Force -ErrorAction SilentlyContinue } } else { Record-Test -Cmdlet 'Update-GeolocationMap pipeline' -Endpoint '(skipped)' -Status 'Skipped' -Detail 'No geo config' } #endregion #region -- Test map HTML export ----------------------------------------------- Write-Host "`n[6/8] Testing Export-GeolocationMapHtml ..." -ForegroundColor Cyan $script:MapOutputPath = Join-Path $env:TEMP "WUGPS-GeoTest-Map-$(Get-Date -Format 'yyyyMMddHHmmss').html" if ($script:GeoAllData -and @($script:GeoAllData).Count -gt 0) { Invoke-Test -Cmdlet 'Export-GeolocationMapHtml' -Endpoint '(HTML generation)' -Test { $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $exportParams = @{ Data = @($script:GeoAllData) OutputPath = $script:MapOutputPath TemplatePath = $templatePath DefaultLat = [double]$testLat DefaultLng = [double]$testLng DefaultZoom = 10 } $resultPath = Export-GeolocationMapHtml @exportParams -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "Map file not created at: $resultPath" } $content = Get-Content $resultPath -Raw if ($content.Length -lt 100) { throw "Map file too small ($($content.Length) bytes)" } if ($content -notmatch 'leaflet') { throw "Map file does not contain Leaflet references" } Write-Verbose "Map generated: $resultPath ($($content.Length) bytes)" } # Verify test device marker is in the generated map Invoke-Test -Cmdlet 'Export-GeolocationMapHtml (verify markers)' -Endpoint '(HTML content check)' -Test { if (-not (Test-Path $script:MapOutputPath)) { throw "Map file missing" } $content = Get-Content $script:MapOutputPath -Raw if ($content -notmatch [regex]::Escape($script:GeoTestDeviceName)) { throw "Test device name not found in generated map HTML" } } } else { Record-Test -Cmdlet 'Export-GeolocationMapHtml' -Endpoint '(skipped)' -Status 'Skipped' -Detail 'No geolocation data available' } # Generate all-providers preview with fake API keys (kept on disk for visual review) Write-Host "`n[7/8] Testing Export-GeolocationMapHtml (all providers) ..." -ForegroundColor Cyan $script:AllProvidersMapPath = Join-Path $env:TEMP "WUGPS-GeoTest-AllProviders-$(Get-Date -Format 'yyyyMMddHHmmss').html" if ($script:GeoAllData -and @($script:GeoAllData).Count -gt 0) { Invoke-Test -Cmdlet 'Export-GeolocationMapHtml (all providers)' -Endpoint '(HTML with all tile keys)' -Test { $templatePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'geolocation\Geolocation-Map-Template.html' $fakeKeys = @{ thunderforest = 'test-tf-key' stadia = 'test-sd-key' maptiler = 'test-mt-key' here = 'test-here-key' mapbox = 'pk.test-mb-key' jawg = 'test-jw-key' tomtom = 'test-tt-key' openweathermap = 'test-owm-key' } $exportParams = @{ Data = @($script:GeoAllData) OutputPath = $script:AllProvidersMapPath TemplatePath = $templatePath DefaultLat = [double]$testLat DefaultLng = [double]$testLng DefaultZoom = 10 TileApiKeys = $fakeKeys } $resultPath = Export-GeolocationMapHtml @exportParams -ErrorAction Stop if (-not (Test-Path $resultPath)) { throw "All-providers map not created at: $resultPath" } $content = Get-Content $resultPath -Raw $missing = @() foreach ($prov in @('thunderforest','stadia','maptiler','here','mapbox','jawg','tomtom','openweathermap')) { if ($content -notmatch $prov) { $missing += $prov } } if ($missing.Count -gt 0) { throw "Missing provider blocks: $($missing -join ', ')" } if ($content -notmatch 'id="settings-panel"') { throw "Settings panel not found in output" } if ($content -notmatch 'Weather Overlays') { throw "Weather overlay box not found (OWM key was provided)" } Write-Host " All-providers map kept at: $resultPath" -ForegroundColor Green } } else { Record-Test -Cmdlet 'Export-GeolocationMapHtml (all providers)' -Endpoint '(skipped)' -Status 'Skipped' -Detail 'No geolocation data available' } #endregion #region -- Cleanup ------------------------------------------------------------ Write-Host "`n[8/8] Cleaning up test artefacts ..." -ForegroundColor Cyan # Remove the test device if ($script:GeoTestDeviceId) { Invoke-Test -Cmdlet 'Remove-WUGDevice (geo device)' -Endpoint 'DELETE /devices/{id}' -Test { Remove-WUGDevice -DeviceId $script:GeoTestDeviceId -Confirm:$false -ErrorAction Stop | Out-Null } } # Remove the test group if ($script:GeoTestGroupId) { Invoke-Test -Cmdlet 'Remove-WUGDeviceGroup (geo group)' -Endpoint 'DELETE /device-groups/{id}' -Test { Remove-WUGDeviceGroup -GroupId $script:GeoTestGroupId -Confirm:$false -ErrorAction Stop | Out-Null } } # Remove generated map file if ($script:MapOutputPath -and (Test-Path $script:MapOutputPath)) { Remove-Item -Path $script:MapOutputPath -Force -ErrorAction SilentlyContinue Write-Host " Removed temp map file: $($script:MapOutputPath)" -ForegroundColor DarkGray } # Remove refresh test map file if ($script:RefreshMapPath -and (Test-Path $script:RefreshMapPath)) { Remove-Item -Path $script:RefreshMapPath -Force -ErrorAction SilentlyContinue Write-Host " Removed temp refresh map file: $($script:RefreshMapPath)" -ForegroundColor DarkGray } # Remove CSV sync test files if ($script:SyncCsvPath -and (Test-Path $script:SyncCsvPath)) { Remove-Item -Path $script:SyncCsvPath -Force -ErrorAction SilentlyContinue Write-Host " Removed temp CSV file: $($script:SyncCsvPath)" -ForegroundColor DarkGray } if ($script:SyncCsvBadPath -and (Test-Path $script:SyncCsvBadPath)) { Remove-Item -Path $script:SyncCsvBadPath -Force -ErrorAction SilentlyContinue Write-Host " Removed temp bad-CSV file: $($script:SyncCsvBadPath)" -ForegroundColor DarkGray } # Remove update map test file if ($script:UpdateMapPath -and (Test-Path $script:UpdateMapPath)) { Remove-Item -Path $script:UpdateMapPath -Force -ErrorAction SilentlyContinue Write-Host " Removed temp update-map file: $($script:UpdateMapPath)" -ForegroundColor DarkGray } # Disconnect Invoke-Test -Cmdlet 'Disconnect-WUGServer' -Endpoint '(session cleanup)' -Test { Disconnect-WUGServer -ErrorAction Stop if ($global:WUGBearerHeaders) { throw "Headers still set after disconnect" } } #endregion #region -- Summary ------------------------------------------------------------ Write-Host "`n============================================================" -ForegroundColor Cyan Write-Host " GEOLOCATION TEST RESULTS SUMMARY" -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan $passed = ($script:TestResults | Where-Object Status -eq 'Pass').Count $failed = ($script:TestResults | Where-Object Status -eq 'Fail').Count $skipped = ($script:TestResults | Where-Object Status -eq 'Skipped').Count $total = $script:TestResults.Count Write-Host "`n Total : $total" -ForegroundColor White Write-Host " Pass : $passed" -ForegroundColor Green Write-Host " Fail : $failed" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' }) Write-Host " Skip : $skipped" -ForegroundColor Yellow if ($failed -gt 0) { Write-Host "`n FAILED TESTS:" -ForegroundColor Red $script:TestResults | Where-Object Status -eq 'Fail' | ForEach-Object { Write-Host " - $($_.Cmdlet) [$($_.Endpoint)]" -ForegroundColor Red if ($_.Detail) { Write-Host " $($_.Detail)" -ForegroundColor DarkRed } } } Write-Host "`n============================================================" -ForegroundColor Cyan $script:TestResults | Format-Table -AutoSize -Property Cmdlet, Endpoint, Status, Detail # Return results object $script:TestResults #endregion # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD79PZPtTr+ff8P # q1tkLbet81fglHSmOU6ybzJH4MRxCKCCJQ0wggVvMIIEV6ADAgECAhBI/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 # BCAMz1e3H3cBJ4lCOIrVvzjP/Uoy7CZG64cmganHLhmKqzANBgkqhkiG9w0BAQEF # AASCAgDKLw3ySO8lzo+d4hxDUL1yJJbn5AkT8GtZK5pHhA0BHD5RbxruellNFH88 # 11h17vy2S+9529cDATOP5YAGDg4tfagbixviQdMuIq6iRMgDGVrXmBk+wG+xZ+8y # oWYB74Hv2cwcUwCtL07fONJIZ4LqzM5VCLBmsm0vPFU1eyTRo5EdO/xZQElbHK/G # BHqBFeD7Gh75s+U9IdQPoET37GYre+FXD9TVGEsIaWw57yVYoUe+vTBvASU7eFN1 # VGSp5n/iClq4J+qlsetN1T6ho8v0+HV9r43hMMFMi8JbsTO6hx/PaVoyuc+npuin # Xbhfy706L+nynEYDXcpvtZ8NJUVzB6KAolOJ4ZpzsVMvdo30DuQO2iss8r0e/FRo # 3api8CiYSpW/eolhkELcY8bk4Q/b+upUlJnpMjvZc7aY3T3mTfx5x/5VihaYxzW/ # +FQqeR1rLr2NDHg71ZZ1kOmBG2dwLf4gz6/3XH0gA2bq3xtaZkrsCkbDThv6YrAF # UMY68qMTb8uZKC/hi+Tmtdo1nSgrJwjTEsDa5Xr4bIpwS3Ui/SlZahL6y0+a6JIT # luSum/b9gGCRs64eFjf8PB0XH96awqKhJIn07+xNWp2rI8PZe+dyoUVY/n63SYBy # 9BFJE/HgszaKa6gJ0drFK1tpp2GezQJJml2ltGQPJvVBiA2uQ6GCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA1MjQxNzQxMjNaMC8GCSqGSIb3DQEJBDEiBCA/IYn8 # 4h9qTLZ0f6dfSeuIQ/d4NsPCCMNH9mG402zSszANBgkqhkiG9w0BAQEFAASCAgBV # sb8XUFcEHCPF86WfPnSvmCT4/WiI9vYbtZmVmPXrtJieM2y4rzt4UtUDURlopNiW # 3grWkaa3Abd6XNjwvWmX5gUmDwlc+CRe4CKfT4mdQ6XIg67IntVICPj2RfeJDi2m # fSUvgtHpKoiRerEzZWA3psMdO1EWQpRtuGL/Z4tQJ9WeTfK6XyH0/HFiurPCyX7q # b4FhSDHApPfeynVoFdutu6l6zA6Pcl4EO83uS4DaDpzc/fCB3KFrNkqFoPfn/WlZ # WFlZQ3GIeCMeQuu7bO9QTd2troWXjU+Od/NqLCBNZSM6IJxUJQxFX/OmEZn7tl0a # VJFaq7ITknxawJUzTbcAYhOkSAq0Esqfj1p6OWYk3Xq65X+DbOx11dPJqoHvSByL # QwxbOxWNnDOwQA3LyTxgV6EqtSskWhOLWd/ldKEl/EulzFBDfvr1I+eTlU+vibrt # ME7D8u41iisAyIKB6ptAqPpsPrSKLQSNfitgw5+DUBkwiHr4qoEVlCuqknqetYx4 # OX1c3I0rO1/pyTjKb6tU5vgKjAYn2cFcOOprRJJaPkfdDs2BGWq7FU+waKdYAorw # A5evXmchy+3W8VzBSyCpgADKQBSr/v7XH1DbLxpIOjt6lvZyjGAqHr8lgP7wT+rE # LYuiCsM/u7bCNU3X05H8JXWPGJcvIV+3GsJr4v5OaQ== # SIG # End signature block |