<# AZA – Step 13: Local Sanity / Regression Smoke Suite (PowerShell 5.1 safe) What it checks (no token leakage): 1) GET /health -> 200 and JSON {"ok": true} (or ok truthy) 2) GET /version -> 200 and JSON has "name" and "build" 3) GET /license/status WITHOUT token -> 401/403 expected 4) GET /license/status WITH token -> 200 and JSON has {valid:boolean, valid_until:number|null} 5) GET /stripe/health -> 200 if present, otherwise WARN if 404 6) GET /openapi.json -> 200 (optional, but useful) Run (while backend is running on 127.0.0.1:8000): powershell -ExecutionPolicy Bypass -File .\deploy\smoke_suite.ps1 Optional: powershell -ExecutionPolicy Bypass -File .\deploy\smoke_suite.ps1 -BaseUrl http://127.0.0.1:8000 #> [CmdletBinding()] param( [string]$BaseUrl = "http://127.0.0.1:8000", [string]$EnvFile = ".\deploy\.env", [string]$DeviceId = "" ) function Load-DotEnv([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { throw "Missing .env file at: $Path" } $map = @{} Get-Content -LiteralPath $Path | ForEach-Object { $line = $_.Trim() if ($line.Length -eq 0) { return } if ($line.StartsWith("#")) { return } $idx = $line.IndexOf("=") if ($idx -lt 1) { return } $k = $line.Substring(0, $idx).Trim() $v = $line.Substring($idx + 1).Trim() if (($v.StartsWith('"') -and $v.EndsWith('"')) -or ($v.StartsWith("'") -and $v.EndsWith("'"))) { $v = $v.Substring(1, $v.Length - 2) } $map[$k] = $v } return $map } function All-TokensFromValue([string]$value) { if (-not $value) { return @() } $value = $value.Trim() return ($value -split "[,\r\n]+" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }) } function Get-TokensFromEnv([hashtable]$envMap) { if ($envMap.ContainsKey("MEDWORK_API_TOKENS")) { return @{ src="MEDWORK_API_TOKENS"; tokens=(All-TokensFromValue $envMap["MEDWORK_API_TOKENS"]) } } if ($envMap.ContainsKey("MEDWORK_API_TOKEN")) { return @{ src="MEDWORK_API_TOKEN"; tokens=(All-TokensFromValue $envMap["MEDWORK_API_TOKEN"]) } } return @{ src=""; tokens=@() } } function Http-Get([string]$url, [hashtable]$headers = $null) { try { $resp = Invoke-WebRequest -Method GET -Uri $url -Headers $headers -TimeoutSec 15 -UseBasicParsing return @{ ok=$true; status=[int]$resp.StatusCode; text=$resp.Content } } catch { $e = $_.Exception $status = $null $text = "" if ($e.Response -and $e.Response.StatusCode) { $status = [int]$e.Response.StatusCode try { $stream = $e.Response.GetResponseStream() if ($stream) { $reader = New-Object System.IO.StreamReader($stream) $text = $reader.ReadToEnd() } } catch { } } return @{ ok=$false; status=$status; text=$text; message=$e.Message } } } function Http-GetRest([string]$url, [hashtable]$headers) { # Use Invoke-RestMethod for auth calls (matches authorized_test.ps1 behavior in PS5.1) try { $obj = Invoke-RestMethod -Method GET -Uri $url -Headers $headers -TimeoutSec 15 $txt = $obj | ConvertTo-Json -Depth 10 -Compress return @{ ok=$true; status=200; text=$txt } } catch { $e = $_.Exception $status = $null $text = "" if ($e.Response -and $e.Response.StatusCode) { $status = [int]$e.Response.StatusCode try { $stream = $e.Response.GetResponseStream() if ($stream) { $reader = New-Object System.IO.StreamReader($stream) $text = $reader.ReadToEnd() } } catch { } } return @{ ok=$false; status=$status; text=$text; message=$e.Message } } } function Parse-Json([string]$s) { try { return (ConvertFrom-Json -InputObject $s -ErrorAction Stop) } catch { return $null } } function Assert([bool]$cond, [string]$msg, [ref]$failCount) { if ($cond) { Write-Host ("PASS: " + $msg) return $true } else { Write-Host ("FAIL: " + $msg) $failCount.Value++ return $false } } function Warn([string]$msg) { Write-Host ("WARN: " + $msg) } function Snip([string]$s, [int]$n = 220) { if (-not $s) { return "" } $t = $s.Trim() if ($t.Length -le $n) { return $t } return ($t.Substring(0, $n) + " ...") } $script:lastDetails = @() function Note-FailDetail([string]$label, $resp) { $st = $resp.status $tx = Snip $resp.text $script:lastDetails += (" " + $label + ": status=" + $st + " body='" + $tx + "'") } $base = $BaseUrl.TrimEnd("/") if (-not $DeviceId) { $DeviceId = "ps-" + $env:COMPUTERNAME + "-" + $env:USERNAME } Write-Host "[AZA] Smoke Suite (Step 13)" Write-Host (" BaseUrl: " + $base) Write-Host (" EnvFile: " + $EnvFile) Write-Host (" DeviceId: " + $DeviceId) Write-Host "" $fail = 0 # Load tokens (but never print them) $envMap = $null try { $envMap = Load-DotEnv $EnvFile } catch { $envMap = @{} } $tk = Get-TokensFromEnv $envMap if (-not $tk.tokens -or $tk.tokens.Count -lt 1) { Warn "No tokens found in .env (MEDWORK_API_TOKENS / MEDWORK_API_TOKEN). Authorized checks will fail." } # 1) /health $r = Http-Get "$base/health" $health200 = ($r.status -eq 200) if (-not $health200) { Note-FailDetail "/health" $r } Assert ($health200) "GET /health returns 200" ([ref]$fail) | Out-Null if ($health200) { $j = Parse-Json $r.text if (-not $j) { Warn "GET /health did not parse as JSON (still OK if your health is plain text)." } elseif ($j.PSObject.Properties.Name -contains "ok") { if (-not ($j.ok -eq $true -or $j.ok -eq 1)) { Warn "GET /health JSON has field ok, but it is not true." } } } Write-Host "" # 2) /version $r = Http-Get "$base/version" $versionOk = $false if ($r.status -eq 200) { $j = Parse-Json $r.text if ($j -and $j.name -and $j.build -ne $null) { $versionOk = $true } } if (-not $versionOk) { Note-FailDetail "/version" $r } Assert ($versionOk) "GET /version returns 200 with fields {name, build}" ([ref]$fail) | Out-Null Write-Host "" # 3) /license/status without token should be 401/403 $r = Http-Get "$base/license/status" $unauthOk = ($r.status -eq 401 -or $r.status -eq 403) Assert ($unauthOk) "GET /license/status without token returns 401/403" ([ref]$fail) | Out-Null Write-Host "" # 4) /license/status with token should be 200 with stable schema $authOk = $false $lastAuthResp = $null if ($tk.tokens -and $tk.tokens.Count -gt 0) { foreach ($t in $tk.tokens) { $h = @{ "X-API-Token" = $t "X-Device-Id" = $DeviceId "Accept" = "application/json" } $r = Http-GetRest "$base/license/status" $h $lastAuthResp = $r if ($r.status -eq 200) { $j = Parse-Json $r.text if ($j -and ($j.valid -is [bool]) -and ($j.PSObject.Properties.Name -contains "valid_until")) { $vu = $j.valid_until $vuOk = ($vu -eq $null) -or ($vu -is [int]) -or ($vu -is [long]) -or ($vu -is [double]) if ($vuOk) { $authOk = $true; break } } } if ($r.status -eq 401 -or $r.status -eq 403) { continue } if ($r.status -eq 404) { break } } } if (-not $authOk -and $lastAuthResp) { Note-FailDetail "/license/status (auth)" $lastAuthResp } Assert ($authOk) "GET /license/status with token returns 200 and schema {valid, valid_until}" ([ref]$fail) | Out-Null Write-Host "" # 5) /stripe/health (optional: warn on 404) $r = Http-Get "$base/stripe/health" if ($r.status -eq 200) { Write-Host "PASS: GET /stripe/health returns 200" } elseif ($r.status -eq 404) { Warn "GET /stripe/health returned 404 (endpoint not present in this build)" } else { Assert $false ("GET /stripe/health returns 200 (or 404 if not included), got " + $r.status) ([ref]$fail) | Out-Null } Write-Host "" # 6) /openapi.json (optional but useful) $r = Http-Get "$base/openapi.json" if ($r.status -eq 200) { Write-Host "PASS: GET /openapi.json returns 200" } elseif ($r.status -eq 404) { Warn "GET /openapi.json returned 404" } else { Warn ("GET /openapi.json unexpected status: " + $r.status) } Write-Host "" if ($fail -eq 0) { Write-Host "RESULT: PASS (Step 13 smoke suite)" exit 0 } else { Write-Host ("RESULT: FAIL (" + $fail + " failing check(s))") if ($script:lastDetails.Count -gt 0) { Write-Host "Failure details (status + short body snippet):" $script:lastDetails | ForEach-Object { Write-Host $_ } } exit 1 }