404 lines
15 KiB
PowerShell
404 lines
15 KiB
PowerShell
<#
|
||
.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 }
|