param( [string]$ServerStatusUrl = "https://qsfera.kusoft.xyz/status.php", [string]$ExpectedServerVersion = "7.0.0", [string]$ExpectedServerEdition = "stable", [string]$AndroidManifestUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/manifest?platform=android&channel=stable", [string]$ExpectedAndroidSlug = "qsfera-mobile", [string]$ExpectedAndroidPackageName = "eu.qsfera.android", [string]$ExpectedAndroidVersion = "1.3.12", [int]$ExpectedAndroidVersionCode = 40, [string]$ExpectedAndroidChannel = "stable", [string]$ExpectedAndroidPlatform = "android", [string]$ExpectedAndroidPackageKind = "apk", [string]$ExpectedAndroidSha256 = "279d00c6a43072caa0b26269f547fb0dc3268088aae5cd9f4ccfbbb485c35ded", [string]$AndroidDownloadUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/download/latest?platform=android&channel=stable", [switch]$VerifyAndroidDownload, [string]$DesktopUpdateFeedUrl = "https://qsfera.kusoft.xyz/desktop/updates/stable.xml", [switch]$RequireDesktopUpdate ) $ErrorActionPreference = "Stop" function Fail($Message) { Write-Error $Message exit 1 } function Assert-AbsoluteHttpsUrl($Name, $Url) { $parsedUrl = $null if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$parsedUrl)) { Fail "$Name must be an absolute URL: $Url" } if ($parsedUrl.Scheme -ne "https") { Fail "$Name must use HTTPS: $Url" } } function Assert-Equal($Name, $Actual, $Expected) { if ([string]$Actual -ne [string]$Expected) { Fail "$Name expected '$Expected' but got '$Actual'." } Write-Host "OK: $Name = $Actual" } function Assert-True($Name, [bool]$Value) { if (-not $Value) { Fail "$Name expected True but got False." } Write-Host "OK: $Name = True" } function Invoke-CheckedWebRequest($Name, $Url) { Assert-AbsoluteHttpsUrl $Name $Url try { $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 30 } catch { Fail "$Name request failed for ${Url}: $($_.Exception.Message)" } if ($response.StatusCode -ne 200) { Fail "$Name must return HTTP 200. Actual status: $($response.StatusCode)." } Write-Host "OK: $Name returned HTTP 200" return $response } function Convert-JsonContent($Name, $Content) { try { return $Content | ConvertFrom-Json -ErrorAction Stop } catch { Fail "$Name returned invalid JSON: $($_.Exception.Message)" } } function Get-RequiredProperty($Object, $Name, $Context) { $property = $Object.PSObject.Properties[$Name] if ($null -eq $property) { Fail "$Context is missing '$Name'." } return $property.Value } function Test-Sha256String($Value) { return [regex]::IsMatch($Value, "^[0-9a-f]{64}$") } Write-Host "Checking QSfera production server status..." $serverResponse = Invoke-CheckedWebRequest "Server status endpoint" $ServerStatusUrl $serverStatus = Convert-JsonContent "Server status endpoint" $serverResponse.Content Assert-True "Server installed" ([bool](Get-RequiredProperty $serverStatus "installed" "Server status")) Assert-Equal "Server maintenance" (Get-RequiredProperty $serverStatus "maintenance" "Server status") "False" Assert-Equal "Server needsDbUpgrade" (Get-RequiredProperty $serverStatus "needsDbUpgrade" "Server status") "False" Assert-Equal "Server edition" (Get-RequiredProperty $serverStatus "edition" "Server status") $ExpectedServerEdition Assert-Equal "Server productversion" (Get-RequiredProperty $serverStatus "productversion" "Server status") $ExpectedServerVersion Write-Host "Checking QSfera Android Argus manifest..." $manifestResponse = Invoke-CheckedWebRequest "Android Argus manifest" $AndroidManifestUrl $manifest = Convert-JsonContent "Android Argus manifest" $manifestResponse.Content Assert-Equal "Android app slug" (Get-RequiredProperty $manifest "slug" "Android manifest") $ExpectedAndroidSlug Assert-Equal "Android package name" (Get-RequiredProperty $manifest "androidPackageName" "Android manifest") $ExpectedAndroidPackageName $release = Get-RequiredProperty $manifest "release" "Android manifest" if ($null -eq $release) { Fail "Android manifest release must not be null." } Assert-Equal "Android release version" (Get-RequiredProperty $release "version" "Android release") $ExpectedAndroidVersion Assert-Equal "Android versionCode" (Get-RequiredProperty $release "androidVersionCode" "Android release") $ExpectedAndroidVersionCode Assert-Equal "Android release channel" (Get-RequiredProperty $release "channel" "Android release") $ExpectedAndroidChannel Assert-Equal "Android release platform" (Get-RequiredProperty $release "platform" "Android release") $ExpectedAndroidPlatform Assert-Equal "Android package kind" (Get-RequiredProperty $release "packageKind" "Android release") $ExpectedAndroidPackageKind $releaseSha256 = Get-RequiredProperty $release "sha256" "Android release" Assert-True "Android SHA-256 shape" (Test-Sha256String $releaseSha256) Assert-Equal "Android SHA-256" $releaseSha256 $ExpectedAndroidSha256 if ($VerifyAndroidDownload) { Write-Host "Checking QSfera Android latest APK download..." Assert-AbsoluteHttpsUrl "Android download URL" $AndroidDownloadUrl $tempFile = [System.IO.Path]::GetTempFileName() try { try { Invoke-WebRequest -Uri $AndroidDownloadUrl -UseBasicParsing -TimeoutSec 120 -OutFile $tempFile | Out-Null } catch { Fail "Android download request failed for ${AndroidDownloadUrl}: $($_.Exception.Message)" } $downloadSize = (Get-Item -LiteralPath $tempFile).Length $expectedSize = Get-RequiredProperty $release "packageSizeBytes" "Android release" Assert-Equal "Android APK size" $downloadSize $expectedSize $downloadSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $tempFile).Hash.ToLowerInvariant() Assert-Equal "Android APK download SHA-256" $downloadSha256 $ExpectedAndroidSha256 } finally { Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue } } Write-Host "Checking QSfera Desktop update feed..." $desktopResponse = Invoke-CheckedWebRequest "Desktop update feed" $DesktopUpdateFeedUrl try { [xml]$desktopFeed = $desktopResponse.Content } catch { Fail "Desktop update feed returned invalid XML: $($_.Exception.Message)" } if ($desktopFeed.DocumentElement.Name -ne "QSfera") { Fail "Desktop update feed root element must be ." } foreach ($elementName in @("version", "versionstring", "web", "downloadurl")) { if ($null -eq $desktopFeed.QSfera.$elementName) { Fail "Desktop update feed is missing <$elementName>." } } $desktopWeb = [string]$desktopFeed.QSfera.web if (-not [string]::IsNullOrWhiteSpace($desktopWeb)) { Assert-AbsoluteHttpsUrl "Desktop update feed " $desktopWeb } $desktopVersion = [string]$desktopFeed.QSfera.version $desktopVersionString = [string]$desktopFeed.QSfera.versionstring $desktopDownloadUrl = [string]$desktopFeed.QSfera.downloadurl if ([string]::IsNullOrWhiteSpace($desktopVersion)) { if ($RequireDesktopUpdate) { Fail "Desktop update feed must advertise a concrete version when -RequireDesktopUpdate is set." } if (-not [string]::IsNullOrWhiteSpace($desktopDownloadUrl)) { Fail "Desktop no-update feed must leave empty when is empty." } Write-Host "OK: Desktop update feed is a valid no-update feed" } else { $parsedVersion = $null if (-not [System.Version]::TryParse($desktopVersion, [ref]$parsedVersion)) { Fail "Desktop update feed must be parseable as a version number." } if ([string]::IsNullOrWhiteSpace($desktopVersionString)) { Fail "Desktop update feed must set when is present." } Assert-AbsoluteHttpsUrl "Desktop update feed " $desktopDownloadUrl $downloadUri = [System.Uri]$desktopDownloadUrl if (-not $downloadUri.AbsolutePath.EndsWith(".msi", [System.StringComparison]::OrdinalIgnoreCase)) { Fail "Desktop Windows updater expects an MSI download URL." } Write-Host "OK: Desktop update feed advertises version $desktopVersionString" } Write-Host "QSfera production endpoint verification passed."