367 lines
12 KiB
PowerShell
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
|
|
}
|
|
}
|