Files
http-client-app-plan/scripts/verify-backend-smoke.ps1
T

367 lines
12 KiB
PowerShell

param(
[int]$ApiPort = 0,
[int]$TargetPort = 0,
[int]$StartupTimeoutSeconds = 30
)
$ErrorActionPreference = "Stop"
$root = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
$backend = Join-Path $root "backend"
$logDir = Join-Path $root "http-client-logs"
$workspace = Join-Path $root "fixtures\workspaces\demo"
$openApiPath = Join-Path $workspace "openapi\demo.yaml"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
function Get-FreeTcpPort {
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 0)
try {
$listener.Start()
return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
}
finally {
$listener.Stop()
}
}
function Test-PortAvailable {
param([int]$Port)
$existing = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue |
Where-Object { $_.State -eq "Listen" } |
Select-Object -First 1
if ($existing) {
throw "Smoke port $Port is already in use by process $($existing.OwningProcess)."
}
}
function Invoke-External {
param(
[string]$FilePath,
[string[]]$Arguments,
[string]$WorkingDirectory = $PWD.Path
)
Push-Location $WorkingDirectory
try {
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$FilePath failed with exit code $LASTEXITCODE."
}
}
finally {
Pop-Location
}
}
function Invoke-Json {
param(
[string]$Method,
[string]$Uri,
[object]$Body = $null
)
$params = @{
Method = $Method
Uri = $Uri
TimeoutSec = 10
}
if ($null -ne $Body) {
$params.ContentType = "application/json"
$params.Body = ($Body | ConvertTo-Json -Depth 50)
}
Invoke-RestMethod @params
}
function Wait-HttpOk {
param(
[string]$Uri,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
try {
$response = Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 2
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
return
}
}
catch {
Start-Sleep -Milliseconds 250
}
} while ((Get-Date) -lt $deadline)
throw "Timed out waiting for $Uri"
}
function Assert-True {
param(
[bool]$Condition,
[string]$Message
)
if (-not $Condition) {
throw $Message
}
}
$ApiPort = if ($ApiPort -eq 0) { Get-FreeTcpPort } else { $ApiPort }
$TargetPort = if ($TargetPort -eq 0) { Get-FreeTcpPort } else { $TargetPort }
$targetSource = Join-Path $backend ("smoke-target-{0}.go" -f ([System.Guid]::NewGuid().ToString("N")))
$targetExe = Join-Path ([System.IO.Path]::GetTempPath()) ("http-client-target-{0}.exe" -f ([System.Guid]::NewGuid().ToString("N")))
$apiExe = Join-Path ([System.IO.Path]::GetTempPath()) ("http-client-api-{0}.exe" -f ([System.Guid]::NewGuid().ToString("N")))
$targetCode = @"
package main
import (
"encoding/json"
"net/http"
"strings"
"time"
"golang.org/x/net/websocket"
)
func writeJSON(w http.ResponseWriter, status int, payload any) {
body, _ := json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write(body)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
body := []byte("event: message\ndata: hello\n\n")
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
})
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, map[string]any{
"users": []map[string]any{{"id": 42, "name": "Zoe"}},
"path": r.URL.RequestURI(),
})
case http.MethodPost:
var body any
_ = json.NewDecoder(r.Body).Decode(&body)
writeJSON(w, http.StatusCreated, map[string]any{"created": true, "body": body})
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/api/private/", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api/private/") {
http.NotFound(w, r)
return
}
writeJSON(w, http.StatusOK, map[string]any{"private": true, "path": r.URL.Path})
})
mux.Handle("/socket", websocket.Handler(func(conn *websocket.Conn) {
defer conn.Close()
for {
var message string
if err := websocket.Message.Receive(conn, &message); err != nil {
return
}
if err := websocket.Message.Send(conn, "echo:"+message); err != nil {
return
}
}
}))
server := &http.Server{
Addr: "127.0.0.1:$TargetPort",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}
"@
$apiProcess = $null
$targetProcess = $null
try {
Test-PortAvailable -Port $ApiPort
Test-PortAvailable -Port $TargetPort
Invoke-External -FilePath "go" -Arguments @("build", "-o", $apiExe, "./cmd/api-client") -WorkingDirectory $backend
Set-Content -LiteralPath $targetSource -Value $targetCode -Encoding UTF8
Invoke-External -FilePath "go" -Arguments @("build", "-o", $targetExe, $targetSource) -WorkingDirectory $backend
$targetProcess = Start-Process -FilePath $targetExe -PassThru -WindowStyle Hidden `
-RedirectStandardOutput (Join-Path $logDir "smoke-target.out.log") `
-RedirectStandardError (Join-Path $logDir "smoke-target.err.log")
$previousAddr = $env:HTTP_CLIENT_ADDR
$env:HTTP_CLIENT_ADDR = "127.0.0.1:$ApiPort"
try {
$apiProcess = Start-Process -FilePath $apiExe -WorkingDirectory $backend -PassThru -WindowStyle Hidden `
-RedirectStandardOutput (Join-Path $logDir "smoke-backend.out.log") `
-RedirectStandardError (Join-Path $logDir "smoke-backend.err.log")
}
finally {
$env:HTTP_CLIENT_ADDR = $previousAddr
}
Wait-HttpOk -Uri "http://127.0.0.1:$TargetPort/api/users" -TimeoutSeconds $StartupTimeoutSeconds
Wait-HttpOk -Uri "http://127.0.0.1:$ApiPort/api/health" -TimeoutSeconds $StartupTimeoutSeconds
$health = Invoke-Json -Method "GET" -Uri "http://127.0.0.1:$ApiPort/api/health"
Assert-True ($health.success -eq $true) "Health check did not return success=true."
$httpContent = @"
@baseUrl = http://127.0.0.1:$TargetPort
# @name listUsers
GET {{baseUrl}}/api/users?limit=2
Accept: application/json
###
# @name createUser
POST {{baseUrl}}/api/users
Content-Type: application/json
{"name":"Zoe"}
"@
$parsed = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/parse" -Body @{
content = $httpContent
filePath = "smoke.http"
}
Assert-True ($parsed.success -eq $true) "Parse endpoint did not return success=true."
Assert-True ($parsed.data.requests.Count -ge 2) "Parse endpoint did not find two requests."
$signature = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/signatures/calculate" -Body @{
algorithm = "hmac-sha256"
data = "hello"
secret = "secret"
encoding = "hex"
}
Assert-True ($signature.data.value -eq "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b") "Unexpected HMAC-SHA256 output."
$execution = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/executions" -Body @{
type = "http"
request = @{
method = "GET"
url = "http://127.0.0.1:$TargetPort/api/users?limit=2"
headers = @{ Accept = "application/json" }
}
}
Assert-True ($execution.success -eq $true) "HTTP execution did not return success=true."
Assert-True ($execution.data.status -eq "succeeded") "HTTP execution did not succeed."
Assert-True ([int]$execution.data.result.statusCode -eq 200) "HTTP execution did not return target status 200."
$batch = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/executions" -Body @{
type = "batch"
requests = @(
@{ method = "GET"; url = "http://127.0.0.1:$TargetPort/api/users" },
@{ method = "POST"; url = "http://127.0.0.1:$TargetPort/api/users"; headers = @{ "Content-Type" = "application/json" }; body = '{"name":"Batch"}' }
)
}
Assert-True ($batch.data.status -eq "succeeded") "Batch execution did not succeed."
Assert-True ($batch.data.children.Count -eq 2) "Batch execution did not return two children."
$load = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/executions" -Body @{
type = "load-test"
request = @{ method = "GET"; url = "http://127.0.0.1:$TargetPort/api/users" }
options = @{ requests = 5; concurrency = 2 }
}
Assert-True ($load.data.status -eq "succeeded") "Load-test execution did not succeed."
$sse = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/executions" -Body @{
type = "sse"
request = @{ method = "GET"; url = "http://127.0.0.1:$TargetPort/events" }
options = @{ maxEvents = 1 }
}
Assert-True ($sse.data.status -eq "succeeded") "SSE execution did not succeed."
Assert-True ($sse.data.result.events.Count -ge 1) "SSE execution did not capture events."
$websocket = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/executions" -Body @{
type = "websocket"
request = @{ method = "GET"; url = "ws://127.0.0.1:$TargetPort/socket" }
options = @{ messages = @("hello") }
}
Assert-True ($websocket.data.status -eq "succeeded") "WebSocket execution did not produce a session summary."
Assert-True ($websocket.data.result.received[0] -eq "echo:hello") "WebSocket execution did not exchange messages with target server."
$history = Invoke-Json -Method "GET" -Uri "http://127.0.0.1:$ApiPort/api/history"
Assert-True ($history.data.items.Count -ge 1) "History endpoint did not return executions."
$events = Invoke-Json -Method "GET" -Uri "http://127.0.0.1:$ApiPort/api/events"
Assert-True ($events.data.events.Count -ge 1) "Global events endpoint did not return events."
$curl = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/export/curl" -Body @{
request = @{
method = "GET"
url = "http://127.0.0.1:$TargetPort/api/users"
headers = @{ Accept = "application/json" }
}
}
Assert-True ($curl.data.curl -match "curl -X GET") "curl export did not generate a GET command."
$postman = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/import/postman" -Body @{
collection = @{
item = @(
@{
name = "Imported users"
request = @{
method = "GET"
url = "http://127.0.0.1:$TargetPort/api/users"
}
}
)
}
}
Assert-True ($postman.data.requests -match "Imported users") "Postman import did not generate .http content."
$openapi = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/openapi/import" -Body @{
path = $openApiPath
baseUrlVariable = "baseUrl"
}
Assert-True ($openapi.success -eq $true) "OpenAPI import did not return success=true."
Assert-True ($openapi.data.requests -match "listUsers") "OpenAPI import did not generate listUsers template."
$mockStatus = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/mock-server/start" -Body @{
workspace = $workspace
port = 0
}
Assert-True ($mockStatus.data.running -eq $true) "Mock server did not start."
$mockResponse = Invoke-WebRequest -Uri "$($mockStatus.data.baseUrl)/api/users" -UseBasicParsing -TimeoutSec 10
Assert-True ($mockResponse.StatusCode -eq 200) "Mock server did not serve the demo rule."
$mockLogs = Invoke-Json -Method "GET" -Uri "http://127.0.0.1:$ApiPort/api/mock-server/hit-logs"
Assert-True ($mockLogs.data.logs.Count -ge 1) "Mock hit logs did not record the request."
$null = Invoke-Json -Method "POST" -Uri "http://127.0.0.1:$ApiPort/api/mock-server/stop"
Write-Host "Backend smoke verification passed."
}
finally {
if ($apiProcess -and -not $apiProcess.HasExited) {
Stop-Process -Id $apiProcess.Id -Force -ErrorAction SilentlyContinue
Wait-Process -Id $apiProcess.Id -Timeout 5 -ErrorAction SilentlyContinue
}
if ($targetProcess -and -not $targetProcess.HasExited) {
Stop-Process -Id $targetProcess.Id -Force -ErrorAction SilentlyContinue
Wait-Process -Id $targetProcess.Id -Timeout 5 -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $targetSource) {
Remove-Item -LiteralPath $targetSource -Force
}
if (Test-Path -LiteralPath $targetExe) {
Remove-Item -LiteralPath $targetExe -Force -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $apiExe) {
Remove-Item -LiteralPath $apiExe -Force -ErrorAction SilentlyContinue
}
}