Files
QSfera/scripts/verify-production-state.ps1
T
2026-06-13 20:07:01 +03:00

226 lines
9.7 KiB
PowerShell

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",
[switch]$RequireAndroidPackageName,
[string]$ExpectedAndroidVersion = "1.3.14",
[int]$ExpectedAndroidVersionCode = 42,
[switch]$RequireAndroidVersionCode,
[string]$ExpectedAndroidChannel = "stable",
[string]$ExpectedAndroidPlatform = "android",
[string]$ExpectedAndroidPackageKind = "apk",
[string]$ExpectedAndroidReleaseId = "4394333f-a84a-4a3b-9bc7-6a48d054d4fe",
[string]$ExpectedAndroidOriginalFileName = "qsfera-mobile-42.apk",
[string]$ExpectedAndroidContentType = "application/vnd.android.package-archive",
[int64]$ExpectedAndroidPackageSizeBytes = 12262908,
[string]$ExpectedAndroidSha256 = "8ddfbb9d3dbff3f7bc1030024a0f03a7928771b06cb373c8996c96fb22ad5a18",
[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
$androidPackageNameProperty = $manifest.PSObject.Properties["androidPackageName"]
if ($null -ne $androidPackageNameProperty) {
Assert-Equal "Android package name" $androidPackageNameProperty.Value $ExpectedAndroidPackageName
} elseif ($RequireAndroidPackageName) {
Fail "Android manifest is missing 'androidPackageName'."
} else {
Write-Host "OK: Android package name is absent; public Argus contract does not require it"
}
$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
$androidVersionCodeProperty = $release.PSObject.Properties["androidVersionCode"]
if ($null -ne $androidVersionCodeProperty) {
Assert-Equal "Android versionCode" $androidVersionCodeProperty.Value $ExpectedAndroidVersionCode
} elseif ($RequireAndroidVersionCode) {
Fail "Android release is missing 'androidVersionCode'."
} else {
Write-Host "OK: Android versionCode is absent; public Argus contract does not require it"
}
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
Assert-Equal "Android release id" (Get-RequiredProperty $release "id" "Android release") $ExpectedAndroidReleaseId
Assert-Equal "Android original file name" (Get-RequiredProperty $release "originalFileName" "Android release") $ExpectedAndroidOriginalFileName
Assert-Equal "Android content type" (Get-RequiredProperty $release "contentType" "Android release") $ExpectedAndroidContentType
Assert-Equal "Android package size" (Get-RequiredProperty $release "packageSizeBytes" "Android release") $ExpectedAndroidPackageSizeBytes
Assert-Equal "Android release download path" (Get-RequiredProperty $release "downloadPath" "Android release") "/api/apps/$ExpectedAndroidSlug/releases/$ExpectedAndroidReleaseId/download"
$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
Assert-Equal "Android APK size" $downloadSize $ExpectedAndroidPackageSizeBytes
$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 <QSfera>."
}
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 <web>" $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 <downloadurl> empty when <version> 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 <version> must be parseable as a version number."
}
if ([string]::IsNullOrWhiteSpace($desktopVersionString)) {
Fail "Desktop update feed must set <versionstring> when <version> is present."
}
Assert-AbsoluteHttpsUrl "Desktop update feed <downloadurl>" $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."