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
This commit is contained in:
Jens Reinemann 2026-05-17 00:08:35 +02:00
parent 56ac9b1425
commit 6bed1214c5

381
run-integration-tests.ps1 Normal file
View file

@ -0,0 +1,381 @@
<#
.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 }