bollwerk/run-integration-tests.ps1
Jens Reinemann 6bed1214c5 test: Integration-Test-Suite fuer Server (Auth, Sync, Messaging, WS)
run-integration-tests.ps1:
- Szenario 1: Auth-Flow (Alice + Bob Login, Token-Validierung)
- Szenario 2: Inventory Sync (PUT, GET, PATCH/items/{id})
- Szenario 3: Messaging + Offline-Delivery (WS-Push nach Reconnect)
- Szenario 4: JWT Refresh (neuer Access-Token bleibt gueltig)
- Szenario 5: Parallele Sessions (Server erkennt Online-Status korrekt)
Alle 15 Tests bestanden gegen VPS-Server (195.246.231.210:8080)
Closes #60
2026-05-17 00:08:35 +02:00

381 lines
15 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
.SYNOPSIS
Krisenvorrat Integration Test Suite
.DESCRIPTION
Testet Auth, Inventory Sync, Messaging (offline delivery), JWT Refresh
und parallele WebSocket-Sessions gegen einen laufenden Server.
.PARAMETER BaseUrl
Server-URL. Standard: http://195.246.231.210:8080
.PARAMETER AlicePassword
Passwort fuer Alice. Standard: alice
.PARAMETER BobPassword
Passwort fuer Bob. Standard: bob
.EXAMPLE
.\run-integration-tests.ps1
.\run-integration-tests.ps1 -BaseUrl "http://localhost:8080"
#>
param(
[string]$BaseUrl = "http://195.246.231.210:8080",
[string]$AliceUser = "alice",
[string]$AlicePassword = "alice",
[string]$BobUser = "bob",
[string]$BobPassword = "bob"
)
$ErrorActionPreference = "Stop"
$WsUrl = $BaseUrl -replace "^http", "ws"
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
$script:PassCount = 0
$script:FailCount = 0
function Pass([string]$msg) {
Write-Host "[PASS] $msg" -ForegroundColor Green
$script:PassCount++
}
function Fail([string]$msg) {
Write-Host "[FAIL] $msg" -ForegroundColor Red
$script:FailCount++
}
function Info([string]$msg) {
Write-Host " $msg" -ForegroundColor DarkGray
}
function Section([string]$title) {
Write-Host ""
Write-Host "--- $title ---" -ForegroundColor Cyan
}
function Invoke-Api {
param(
[string]$Method,
[string]$Path,
[object]$Body = $null,
[string]$Token = $null
)
$headers = @{ "Content-Type" = "application/json" }
if ($Token) { $headers["Authorization"] = "Bearer $Token" }
$bodyJson = if ($Body) { $Body | ConvertTo-Json -Depth 10 -Compress } else { $null }
try {
$response = Invoke-RestMethod -Method $Method `
-Uri "$BaseUrl$Path" `
-Body $bodyJson `
-Headers $headers `
-ErrorAction Stop
return $response
} catch {
$status = $_.Exception.Response.StatusCode.value__
throw "HTTP $status bei $Method $Path"
}
}
# WebSocket-Verbindung oeffnen (gibt ClientWebSocket zurueck)
function Open-WebSocket([string]$token) {
$ws = New-Object System.Net.WebSockets.ClientWebSocket
$uri = [Uri]"$WsUrl/ws/sync?token=$token"
$ws.ConnectAsync($uri, [System.Threading.CancellationToken]::None).Wait()
return $ws
}
# Alle anstehenden Frames eines WebSocket empfangen (waitSeconds: max. Wartezeit)
# WICHTIG: CancellationToken darf NICHT in ReceiveAsync verwendet werden
# das abortet die gesamte .NET ClientWebSocket-Verbindung.
# Stattdessen: Task.Wait(timeout) als Deadline-Mechanismus.
function Receive-WsMessages([System.Net.WebSockets.ClientWebSocket]$ws, [int]$waitSeconds = 4) {
$messages = @()
$buffer = New-Object byte[] 8192
$deadline = [DateTime]::UtcNow.AddSeconds($waitSeconds)
while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
$remaining = [int]($deadline - [DateTime]::UtcNow).TotalMilliseconds
if ($remaining -le 0) { break }
$seg = [ArraySegment[byte]]::new($buffer)
try {
$task = $ws.ReceiveAsync($seg, [System.Threading.CancellationToken]::None)
$completed = $task.Wait($remaining)
if (-not $completed) { break } # Deadline abgelaufen, kein Frame
$result = $task.Result
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { break }
if ($result.Count -gt 0) {
$text = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count)
try { $messages += $text | ConvertFrom-Json } catch { }
}
} catch {
break
}
}
return $messages
}
function Close-WebSocket([System.Net.WebSockets.ClientWebSocket]$ws) {
try { $ws.Abort() } catch { }
$ws.Dispose()
}
# ---------------------------------------------------------------------------
# Szenario 1: Auth-Flow
# ---------------------------------------------------------------------------
Section "Szenario 1: Auth-Flow"
$aliceTokens = $null
$bobTokens = $null
try {
$aliceTokens = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $AliceUser; password = $AlicePassword }
Pass "Auth: Alice login (userId=$($aliceTokens.userId))"
} catch { Fail "Auth: Alice login - $_" }
try {
$bobTokens = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword }
Pass "Auth: Bob login (userId=$($bobTokens.userId))"
} catch { Fail "Auth: Bob login - $_" }
# Geschuetzten Endpoint mit Token aufrufen
if ($aliceTokens) {
try {
$inv = Invoke-Api -Method GET -Path "/api/inventory" -Token $aliceTokens.accessToken
Pass "Auth: Alice-Token fuer geschuetzten Endpoint gueltig"
} catch { Fail "Auth: Alice-Token ungueltig - $_" }
}
# ---------------------------------------------------------------------------
# Szenario 2: Inventory Sync (Alice laedt hoch und liest zurueck)
# ---------------------------------------------------------------------------
Section "Szenario 2: Inventory Sync"
$itemId = [System.Guid]::NewGuid().ToString()
$testInventory = @{
version = 1
categories = @(@{ id = 1; name = "Lebensmittel" })
locations = @(@{ id = 1; name = "Keller" })
items = @(@{
id = $itemId
name = "Testkonserve"
categoryId = 1
quantity = 12.0
unit = "Stueck"
unitPrice = 1.5
kcalPerKg = $null
expiryDate = "2027-12-31"
locationId = 1
notes = "Integrationstest"
lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
})
settings = @()
}
if ($aliceTokens) {
try {
$uploaded = Invoke-Api -Method PUT -Path "/api/inventory" -Body $testInventory -Token $aliceTokens.accessToken
Pass "Inventory Sync: Alice laedt Inventar hoch ($($uploaded.items.Count) Item(s))"
} catch { Fail "Inventory Sync: Upload fehlgeschlagen - $_" }
try {
$fetched = Invoke-Api -Method GET -Path "/api/inventory" -Token $aliceTokens.accessToken
$item = $fetched.items | Where-Object { $_.id -eq $itemId }
if ($item -and $item.name -eq "Testkonserve" -and $item.quantity -eq 12.0) {
Pass "Inventory Sync: Abgerufenes Inventar konsistent (name=$($item.name), qty=$($item.quantity))"
} else {
Fail "Inventory Sync: Abgerufenes Item stimmt nicht ueberein"
}
} catch { Fail "Inventory Sync: GET fehlgeschlagen - $_" }
# PATCH eines einzelnen Items
try {
$patch = @{
id = $itemId; name = "Testkonserve"; categoryId = 1
quantity = 24.0; unit = "Stueck"; unitPrice = 1.5
kcalPerKg = $null; expiryDate = "2027-12-31"; locationId = 1
notes = "Nach PATCH"; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}
Invoke-Api -Method PATCH -Path "/api/inventory/items/$itemId" -Body $patch -Token $aliceTokens.accessToken | Out-Null
$fetched2 = Invoke-Api -Method GET -Path "/api/inventory" -Token $aliceTokens.accessToken
$patched = $fetched2.items | Where-Object { $_.id -eq $itemId }
if ($patched -and $patched.quantity -eq 24.0) {
Pass "Inventory Sync: PATCH item erfolgreich (qty=$($patched.quantity))"
} else {
Fail "Inventory Sync: PATCH-Ergebnis nicht korrekt"
}
} catch { Fail "Inventory Sync: PATCH fehlgeschlagen - $_" }
}
# ---------------------------------------------------------------------------
# Szenario 3: Messaging + Offline Delivery
# ---------------------------------------------------------------------------
Section "Szenario 3: Messaging + Offline Delivery"
if ($aliceTokens -and $bobTokens) {
$bobId = $bobTokens.userId
$aliceId = $aliceTokens.userId
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
# Alice sendet 3 Nachrichten an Bob (Bob hat keine aktive WS-Verbindung)
$sentIds = @()
$allSent = $true
for ($i = 1; $i -le 3; $i++) {
try {
$msg = Invoke-Api -Method POST -Path "/api/messages" -Token $aliceTokens.accessToken -Body @{
receiverId = $bobId
body = "Hallo Bob, Nachricht $i von Alice"
sentAt = $now + $i
}
$sentIds += $msg.id
} catch {
Fail "Messaging: Alice sendet Nachricht $i - $_"
$allSent = $false
}
}
if ($allSent) { Pass "Messaging: Alice sendet 3 Nachrichten an Bob (offline)" }
# Bob verbindet sich per WebSocket -> muss die 3 unzugestellten Nachrichten empfangen
try {
$bobWs = Open-WebSocket -token $bobTokens.accessToken
Info "Bob WebSocket verbunden (State=$($bobWs.State))"
Start-Sleep -Milliseconds 500
$received = Receive-WsMessages -ws $bobWs
Close-WebSocket -ws $bobWs
$newMsgs = $received | Where-Object { $_.type -eq "new_message" }
if ($newMsgs.Count -ge 3) {
Pass "Messaging: Bob empfaengt $($newMsgs.Count) Nachrichten nach WS-Connect (Offline-Delivery)"
} else {
Fail "Messaging: Bob empfaengt nur $($newMsgs.Count)/3 Nachrichten (erwartet 3)"
}
} catch { Fail "Messaging: Bob WebSocket - $_" }
# Bob antwortet 2 Nachrichten (Alice ist jetzt offline)
$replySent = $true
for ($i = 1; $i -le 2; $i++) {
try {
Invoke-Api -Method POST -Path "/api/messages" -Token $bobTokens.accessToken -Body @{
receiverId = $aliceId
body = "Antwort $i von Bob"
sentAt = $now + 100 + $i
} | Out-Null
} catch {
Fail "Messaging: Bob sendet Antwort $i - $_"
$replySent = $false
}
}
if ($replySent) { Pass "Messaging: Bob sendet 2 Antworten (Alice offline)" }
# Alice verbindet sich -> empfaengt Bobs Antworten als Offline-Delivery
try {
$aliceWs = Open-WebSocket -token $aliceTokens.accessToken
Start-Sleep -Milliseconds 500
$aliceReceived = Receive-WsMessages -ws $aliceWs
Close-WebSocket -ws $aliceWs
$aliceMsgs = $aliceReceived | Where-Object { $_.type -eq "new_message" }
if ($aliceMsgs.Count -ge 2) {
Pass "Messaging: Alice empfaengt $($aliceMsgs.Count) Offline-Nachrichten von Bob nach Reconnect"
} else {
Fail "Messaging: Alice empfaengt nur $($aliceMsgs.Count)/2 Nachrichten (erwartet 2)"
}
} catch { Fail "Messaging: Alice WebSocket Reconnect - $_" }
# Konversations-Verlauf per HTTP
try {
$conv = Invoke-Api -Method GET -Path "/api/messages/$bobId" -Token $aliceTokens.accessToken
if ($conv.Count -ge 5) {
Pass "Messaging: Konversationsverlauf enthaelt $($conv.Count) Nachrichten"
} else {
Fail "Messaging: Konversation enthaelt nur $($conv.Count) Nachrichten (erwartet >=5)"
}
} catch { Fail "Messaging: Konversationsverlauf abruf - $_" }
}
# ---------------------------------------------------------------------------
# Szenario 4: JWT Refresh
# ---------------------------------------------------------------------------
Section "Szenario 4: JWT Refresh"
if ($aliceTokens) {
try {
$refreshed = Invoke-Api -Method POST -Path "/api/auth/refresh" -Body @{ refreshToken = $aliceTokens.refreshToken }
if ($refreshed.accessToken -and $refreshed.accessToken -ne $aliceTokens.accessToken) {
Pass "JWT Refresh: Neuer Access-Token erhalten"
# Alten Token sollte unbrauchbar sein nicht zwingend pruefbar; neuer Token muss funktionieren
$inv = Invoke-Api -Method GET -Path "/api/inventory" -Token $refreshed.accessToken
Pass "JWT Refresh: Neuer Access-Token fuer geschuetzten Endpoint gueltig"
} else {
Fail "JWT Refresh: Kein neuer Token erhalten"
}
} catch { Fail "JWT Refresh: $_" }
}
# ---------------------------------------------------------------------------
# Szenario 5: Parallele Sessions
# ---------------------------------------------------------------------------
Section "Szenario 5: Parallele Sessions"
if ($aliceTokens -and $bobTokens) {
$ws1 = $null
$ws2 = $null
try {
# Alice oeffnet 2 gleichzeitige WebSocket-Verbindungen
$ws1 = Open-WebSocket -token $aliceTokens.accessToken
$ws2 = Open-WebSocket -token $aliceTokens.accessToken
Info "Alice: 2 WebSocket-Verbindungen offen (State1=$($ws1.State), State2=$($ws2.State))"
if ($ws1.State -eq "Open" -and $ws2.State -eq "Open") {
Pass "Parallele Sessions: Beide Verbindungen geoeffnet"
} else {
Fail "Parallele Sessions: Nicht beide Verbindungen offen"
}
# Warte bis Sessions serverseitig registriert sind
Start-Sleep -Milliseconds 1500
# Bob schickt Nachricht an Alice (sie ist online mit 2 Sessions)
Invoke-Api -Method POST -Path "/api/messages" -Token $bobTokens.accessToken -Body @{
receiverId = $aliceTokens.userId
body = "Broadcast-Test von Bob"
sentAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
} | Out-Null
# Kurz warten, dann beide Verbindungen schliessen
Start-Sleep -Milliseconds 500
Close-WebSocket -ws $ws1; $ws1 = $null
Close-WebSocket -ws $ws2; $ws2 = $null
# Indirekter Test: Wenn der Server Alice als "online" erkannt hat,
# wurde die Nachricht als delivered markiert (NICHT in undelivered-Queue).
# Eine neue WS-Verbindung von Alice darf KEINE undelivered messages empfangen.
Start-Sleep -Milliseconds 500
$ws3 = Open-WebSocket -token $aliceTokens.accessToken
Start-Sleep -Milliseconds 800
$pending = Receive-WsMessages -ws $ws3 -waitSeconds 2
Close-WebSocket -ws $ws3; $ws3 = $null
$undeliveredCount = ($pending | Where-Object { $_.type -eq "new_message" }).Count
if ($undeliveredCount -eq 0) {
Pass "Parallele Sessions: Server hat Alice korrekt als online erkannt - Nachricht als delivered markiert (nicht in Offline-Queue)"
} else {
Fail "Parallele Sessions: Server hat $undeliveredCount Nachricht(en) als undelivered gespeichert, obwohl Alice online war"
}
} catch { Fail "Parallele Sessions: $_" }
finally {
if ($ws1) { Close-WebSocket -ws $ws1 }
if ($ws2) { Close-WebSocket -ws $ws2 }
}
}
# ---------------------------------------------------------------------------
# Ergebnis
# ---------------------------------------------------------------------------
$total = $script:PassCount + $script:FailCount
Write-Host ""
Write-Host "======================================" -ForegroundColor White
Write-Host "ERGEBNIS: $($script:PassCount)/$total Tests bestanden" -ForegroundColor $(if ($script:FailCount -eq 0) { "Green" } else { "Yellow" })
Write-Host "======================================" -ForegroundColor White
if ($script:FailCount -gt 0) { exit 1 } else { exit 0 }