<# .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. # Gibt immer ein Array zurueck (,$ verhindert PS-Unwrapping bei 1 Element). 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 # Komma verhindert PS-Unwrapping bei 1 Element } 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 } } } # --------------------------------------------------------------------------- # Szenario 6: Bob-Generaltest (10 Items, PATCH, WebSocket-Push) # --------------------------------------------------------------------------- Section "Szenario 6: Bob-Generaltest" if ($bobTokens) { # Artikel-IDs fuer das Haupt-Szenario $dosenBrotId = [System.Guid]::NewGuid().ToString() $wasserId = [System.Guid]::NewGuid().ToString() $thunfischId = [System.Guid]::NewGuid().ToString() $nudelnId = [System.Guid]::NewGuid().ToString() $toilettenPapierId = [System.Guid]::NewGuid().ToString() $seifenId = [System.Guid]::NewGuid().ToString() $muesliId = [System.Guid]::NewGuid().ToString() $apfelsaftId = [System.Guid]::NewGuid().ToString() $kerzenId = [System.Guid]::NewGuid().ToString() $pflasterId = [System.Guid]::NewGuid().ToString() $bob10Items = @{ version = 1 categories = @( @{ id = 10; name = "Lebensmittel" } @{ id = 11; name = "Getraenke" } @{ id = 12; name = "Hygiene" } ) locations = @( @{ id = 10; name = "Keller" } @{ id = 11; name = "Badezimmer" } ) items = @( @{ id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10; quantity = 5.0; unit = "Stueck"; unitPrice = 2.0; kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $wasserId; name = "Mineralwasser"; categoryId = 11; quantity = 24.0; unit = "Stueck"; unitPrice = 0.5; kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $thunfischId; name = "Thunfisch"; categoryId = 10; quantity = 8.0; unit = "Dose"; unitPrice = 1.2; kcalPerKg = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $nudelnId; name = "Nudeln"; categoryId = 10; quantity = 3.0; unit = "Packung"; unitPrice = 0.9; kcalPerKg = $null; expiryDate = "2029-01-01"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $toilettenPapierId; name = "Toilettenpapier"; categoryId = 12; quantity = 12.0; unit = "Rollen"; unitPrice = 0.3; kcalPerKg = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $seifenId; name = "Seife"; categoryId = 12; quantity = 4.0; unit = "Stueck"; unitPrice = 1.5; kcalPerKg = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $muesliId; name = "Muesliegel"; categoryId = 10; quantity = 20.0; unit = "Stueck"; unitPrice = 0.8; kcalPerKg = $null; expiryDate = "2028-03-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $apfelsaftId; name = "Apfelsaft"; categoryId = 11; quantity = 6.0; unit = "Liter"; unitPrice = 1.0; kcalPerKg = $null; expiryDate = "2028-09-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $kerzenId; name = "Kerzen"; categoryId = 10; quantity = 10.0; unit = "Stueck"; unitPrice = 1.5; kcalPerKg = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } @{ id = $pflasterId; name = "Erste-Hilfe-Pflaster"; categoryId = 12; quantity = 2.0; unit = "Packung"; unitPrice = 3.5; kcalPerKg = $null; expiryDate = "2029-06-30"; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } ) settings = @() } # ---- Schritt 2: Bob pusht 10 Artikel ---- try { $uploaded = Invoke-Api -Method PUT -Path "/api/inventory" -Body $bob10Items -Token $bobTokens.accessToken Pass "Bob-Generaltest: 10 Artikel hochgeladen ($($uploaded.items.Count) Items)" } catch { Fail "Bob-Generaltest: PUT 10 Items fehlgeschlagen - $_" } try { $fetched = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken $itemCount = $fetched.items.Count $catCount = $fetched.categories.Count $locCount = $fetched.locations.Count if ($itemCount -eq 10 -and $catCount -eq 3 -and $locCount -eq 2) { Pass "Bob-Generaltest: GET liefert $itemCount Items, $catCount Kategorien, $locCount Lagerorte" } else { Fail "Bob-Generaltest: Erwartet 10/3/2, erhalten $itemCount/$catCount/$locCount" } } catch { Fail "Bob-Generaltest: GET nach PUT fehlgeschlagen - $_" } # ---- Schritt 3: PATCH Dosenbrot qty 5 → 10 ---- try { $patchBody = @{ id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10 quantity = 10.0; unit = "Stueck"; unitPrice = 2.0 kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10 notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } Invoke-Api -Method PATCH -Path "/api/inventory/items/$dosenBrotId" -Body $patchBody -Token $bobTokens.accessToken | Out-Null $f = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken $dosenbrot = $f.items | Where-Object { $_.id -eq $dosenBrotId } if ($dosenbrot -and $dosenbrot.quantity -eq 10.0) { Pass "Bob-Generaltest: PATCH Dosenbrot qty=10 erfolgreich" } else { Fail "Bob-Generaltest: PATCH Dosenbrot qty=$($dosenbrot.quantity), erwartet 10" } } catch { Fail "Bob-Generaltest: PATCH fehlgeschlagen - $_" } # ---- Schritt 4: PUT mit 11 Items (Salzcracker neu) ---- $salzcrackerId = [System.Guid]::NewGuid().ToString() $bob11Items = @{ version = 1 categories = $bob10Items.categories locations = $bob10Items.locations items = $bob10Items.items + @( @{ id = $salzcrackerId; name = "Salzcracker"; categoryId = 10; quantity = 5.0; unit = "Packung"; unitPrice = 1.2; kcalPerKg = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } ) settings = @() } try { Invoke-Api -Method PUT -Path "/api/inventory" -Body $bob11Items -Token $bobTokens.accessToken | Out-Null $f = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken if ($f.items.Count -eq 11) { Pass "Bob-Generaltest: PUT mit 11 Items - GET liefert 11 Items" } else { Fail "Bob-Generaltest: Erwartet 11 Items, erhalten $($f.items.Count)" } } catch { Fail "Bob-Generaltest: PUT 11 Items fehlgeschlagen - $_" } # ---- Schritt 5: Server-seitige Aenderung loest WebSocket-Push aus ---- $bobWsMain = $null try { $bobWsMain = Open-WebSocket -token $bobTokens.accessToken Start-Sleep -Milliseconds 800 # Zweiter Bob-Client patcht Dosenbrot → qty 15 $bobTokens2 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword } $patchBody2 = @{ id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10 quantity = 15.0; unit = "Stueck"; unitPrice = 2.0 kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10 notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } Invoke-Api -Method PATCH -Path "/api/inventory/items/$dosenBrotId" -Body $patchBody2 -Token $bobTokens2.accessToken | Out-Null $events = Receive-WsMessages -ws $bobWsMain -waitSeconds 5 Info "WS-Events Schritt 5: $($events.Count) Event(s) - $($events | ConvertTo-Json -Compress)" $invUpdated = @($events | Where-Object { $_.type -eq "inventoryUpdated" -and $_.itemId -eq $dosenBrotId }) if ($invUpdated.Count -gt 0) { Pass "Bob-Generaltest: WS empfaengt inventoryUpdated-Event mit korrekter itemId" } else { Fail "Bob-Generaltest: WS empfaengt kein inventoryUpdated-Event (events: $($events.Count), raw: $($events | ConvertTo-Json -Compress))" } } catch { Fail "Bob-Generaltest: WebSocket-Push fehlgeschlagen - $_" } finally { if ($bobWsMain) { Close-WebSocket -ws $bobWsMain; $bobWsMain = $null } } # ---- Schritt 6: Full-Sync-Push bei PUT ---- $bobWsFull = $null try { $bobWsFull = Open-WebSocket -token $bobTokens.accessToken Start-Sleep -Milliseconds 800 $bobTokens3 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword } Invoke-Api -Method PUT -Path "/api/inventory" -Body $bob11Items -Token $bobTokens3.accessToken | Out-Null $events2 = Receive-WsMessages -ws $bobWsFull -waitSeconds 5 Info "WS-Events Schritt 6: $($events2.Count) Event(s) - $($events2 | ConvertTo-Json -Compress)" $fullSync = @($events2 | Where-Object { $_.type -eq "fullSyncRequired" }) if ($fullSync.Count -gt 0) { Pass "Bob-Generaltest: WS empfaengt fullSyncRequired-Event nach PUT" } else { Fail "Bob-Generaltest: WS empfaengt kein fullSyncRequired-Event (events: $($events2.Count), raw: $($events2 | ConvertTo-Json -Compress))" } } catch { Fail "Bob-Generaltest: Full-Sync-Push fehlgeschlagen - $_" } finally { if ($bobWsFull) { Close-WebSocket -ws $bobWsFull; $bobWsFull = $null } } # ---- T1: PATCH auf unbekannte Item-ID → 404 ---- try { $unknownId = [System.Guid]::NewGuid().ToString() $patchT1 = @{ id = $unknownId; name = "Unbekannt"; categoryId = 10 quantity = 1.0; unit = "Stueck"; unitPrice = 0.0 kcalPerKg = $null; expiryDate = $null; locationId = 10 notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } try { Invoke-Api -Method PATCH -Path "/api/inventory/items/$unknownId" -Body $patchT1 -Token $bobTokens.accessToken | Out-Null Fail "T1 bob_patchNonExistentItem: 404 erwartet, kein Fehler erhalten" } catch { if ($_ -match "404") { Pass "T1 bob_patchNonExistentItem: PATCH unbekannte ID → 404" } else { Fail "T1 bob_patchNonExistentItem: Unerwarteter Fehler: $_" } } } catch { Fail "T1 bob_patchNonExistentItem: $_" } # ---- T5: Bob sieht nicht Alices Inventar ---- if ($aliceTokens) { try { $aliceFetch = Invoke-Api -Method GET -Path "/api/inventory" -Token $aliceTokens.accessToken $bobFetch = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken $aliceIds = @($aliceFetch.items | Select-Object -ExpandProperty id) $bobIds = @($bobFetch.items | Select-Object -ExpandProperty id) $overlap = $aliceIds | Where-Object { $bobIds -contains $_ } if ($overlap.Count -eq 0) { Pass "T5 bob_cannotSeeAlice_inventory: Bob und Alice haben separate Inventare" } else { Fail "T5 bob_cannotSeeAlice_inventory: $($overlap.Count) Item-IDs ueberschneiden sich" } } catch { Fail "T5 bob_cannotSeeAlice_inventory: $_" } } # ---- T6: Ungueltiger Token → 401 ---- try { $invalidToken = "invalid.jwt.token" try { Invoke-Api -Method GET -Path "/api/inventory" -Token $invalidToken | Out-Null Fail "T6 bob_invalidToken: GET sollte 401 liefern" } catch { if ($_ -match "401") { Pass "T6 bob_invalidToken: GET /api/inventory → 401" } else { Fail "T6 bob_invalidToken: Unerwarteter Fehler bei GET: $_" } } $t6PatchId = [System.Guid]::NewGuid().ToString() $t6Body = @{ id = $t6PatchId; name = "X"; categoryId = 10; quantity = 1.0; unit = "x"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } try { Invoke-Api -Method PATCH -Path "/api/inventory/items/$t6PatchId" -Body $t6Body -Token $invalidToken | Out-Null Fail "T6 bob_invalidToken: PATCH sollte 401 liefern" } catch { if ($_ -match "401") { Pass "T6 bob_invalidToken: PATCH /api/inventory/items → 401" } else { Fail "T6 bob_invalidToken: Unerwarteter Fehler bei PATCH: $_" } } } catch { Fail "T6 bob_invalidToken: $_" } # ---- T2: PUT mit leeren Listen → GET liefert 0 Items ---- try { $emptyInventory = @{ version = 1; categories = @(); locations = @(); items = @(); settings = @() } Invoke-Api -Method PUT -Path "/api/inventory" -Body $emptyInventory -Token $bobTokens.accessToken | Out-Null $emptyFetch = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken if ($emptyFetch.items.Count -eq 0) { Pass "T2 bob_pushEmptyInventory: PUT leer → GET liefert 0 Items" } else { Fail "T2 bob_pushEmptyInventory: Erwartet 0 Items, erhalten $($emptyFetch.items.Count)" } } catch { Fail "T2 bob_pushEmptyInventory: $_" } # ---- T3: Zwei PUTs hintereinander → Zweites ueberschreibt ---- try { $firstId = [System.Guid]::NewGuid().ToString() $secondId = [System.Guid]::NewGuid().ToString() $firstPut = @{ version = 1 categories = @(@{ id = 20; name = "KatA" }); locations = @(@{ id = 20; name = "OrtA" }) items = @(@{ id = $firstId; name = "ErstesPUT"; categoryId = 20; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 20; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }) settings = @() } $secondPut = @{ version = 1 categories = @(@{ id = 21; name = "KatB" }); locations = @(@{ id = 21; name = "OrtB" }) items = @(@{ id = $secondId; name = "ZweitesPUT"; categoryId = 21; quantity = 2.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 21; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }) settings = @() } Invoke-Api -Method PUT -Path "/api/inventory" -Body $firstPut -Token $bobTokens.accessToken | Out-Null Invoke-Api -Method PUT -Path "/api/inventory" -Body $secondPut -Token $bobTokens.accessToken | Out-Null $t3Fetch = Invoke-Api -Method GET -Path "/api/inventory" -Token $bobTokens.accessToken $hasFirst = @($t3Fetch.items | Where-Object { $_.id -eq $firstId }).Count -gt 0 $hasSecond = @($t3Fetch.items | Where-Object { $_.id -eq $secondId }).Count -gt 0 if (-not $hasFirst -and $hasSecond) { Pass "T3 bob_putOverwritesPreviousData: Zweites PUT ueberschreibt erstes" } else { Fail "T3 bob_putOverwritesPreviousData: hasFirst=$hasFirst, hasSecond=$hasSecond" } } catch { Fail "T3 bob_putOverwritesPreviousData: $_" } # ---- T4: Bob patcht selbst → empfaengt trotzdem WS-Event ---- $t4WS = $null try { $t4ItemId = [System.Guid]::NewGuid().ToString() $t4Inv = @{ version = 1; categories = @(@{ id = 30; name = "KatT4" }); locations = @(@{ id = 30; name = "OrtT4" }) items = @(@{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }) settings = @() } Invoke-Api -Method PUT -Path "/api/inventory" -Body $t4Inv -Token $bobTokens.accessToken | Out-Null $t4WS = Open-WebSocket -token $bobTokens.accessToken Start-Sleep -Milliseconds 800 $t4Patch = @{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 5.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } Invoke-Api -Method PATCH -Path "/api/inventory/items/$t4ItemId" -Body $t4Patch -Token $bobTokens.accessToken | Out-Null $t4Events = Receive-WsMessages -ws $t4WS -waitSeconds 5 Info "WS-Events T4: $($t4Events.Count) Event(s) - $($t4Events | ConvertTo-Json -Compress)" if (@($t4Events | Where-Object { $_.type -eq "inventoryUpdated" }).Count -gt 0) { Pass "T4 bob_websocketReceivesOwnPatch: Bob empfaengt inventoryUpdated nach eigenem PATCH" } else { Fail "T4 bob_websocketReceivesOwnPatch: Kein WS-Event empfangen (events: $($t4Events.Count), raw: $($t4Events | ConvertTo-Json -Compress))" } } catch { Fail "T4 bob_websocketReceivesOwnPatch: $_" } finally { if ($t4WS) { Close-WebSocket -ws $t4WS; $t4WS = $null } } # ---- T7: Bob oeffnet 2 WS-Verbindungen → beide empfangen Events ---- $t7ws1 = $null $t7ws2 = $null try { $t7ItemId = [System.Guid]::NewGuid().ToString() $t7Inv = @{ version = 1; categories = @(@{ id = 40; name = "KatT7" }); locations = @(@{ id = 40; name = "OrtT7" }) items = @(@{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }) settings = @() } Invoke-Api -Method PUT -Path "/api/inventory" -Body $t7Inv -Token $bobTokens.accessToken | Out-Null $t7ws1 = Open-WebSocket -token $bobTokens.accessToken $t7ws2 = Open-WebSocket -token $bobTokens.accessToken Start-Sleep -Milliseconds 1200 $bobTokens4 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword } $t7Patch = @{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 9.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } Invoke-Api -Method PATCH -Path "/api/inventory/items/$t7ItemId" -Body $t7Patch -Token $bobTokens4.accessToken | Out-Null $t7ev1 = Receive-WsMessages -ws $t7ws1 -waitSeconds 5 $t7ev2 = Receive-WsMessages -ws $t7ws2 -waitSeconds 5 Info "WS-Events T7-WS1: $($t7ev1.Count) - $($t7ev1 | ConvertTo-Json -Compress)" Info "WS-Events T7-WS2: $($t7ev2.Count) - $($t7ev2 | ConvertTo-Json -Compress)" $got1 = @($t7ev1 | Where-Object { $_.type -eq "inventoryUpdated" }).Count -gt 0 $got2 = @($t7ev2 | Where-Object { $_.type -eq "inventoryUpdated" }).Count -gt 0 if ($got1 -and $got2) { Pass "T7 bob_multipleWebSocketSessions: Beide WS-Sessions empfangen inventoryUpdated" } else { Fail "T7 bob_multipleWebSocketSessions: WS1=$got1, WS2=$got2" } } catch { Fail "T7 bob_multipleWebSocketSessions: $_" } finally { if ($t7ws1) { Close-WebSocket -ws $t7ws1; $t7ws1 = $null } if ($t7ws2) { Close-WebSocket -ws $t7ws2; $t7ws2 = $null } } # ---- T8: Bob disconnected, dritter Client patcht → kein Server-Fehler ---- try { $t8ItemId = [System.Guid]::NewGuid().ToString() $t8Inv = @{ version = 1; categories = @(@{ id = 50; name = "KatT8" }); locations = @(@{ id = 50; name = "OrtT8" }) items = @(@{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }) settings = @() } Invoke-Api -Method PUT -Path "/api/inventory" -Body $t8Inv -Token $bobTokens.accessToken | Out-Null $t8ws = Open-WebSocket -token $bobTokens.accessToken Close-WebSocket -ws $t8ws; $t8ws = $null Start-Sleep -Milliseconds 600 $bobTokens5 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword } $t8Patch = @{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 7.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } try { Invoke-Api -Method PATCH -Path "/api/inventory/items/$t8ItemId" -Body $t8Patch -Token $bobTokens5.accessToken | Out-Null Pass "T8 bob_patchAfterDisconnect: PATCH nach Bob-Disconnect liefert keinen Server-Fehler" } catch { Fail "T8 bob_patchAfterDisconnect: PATCH fehlgeschlagen - $_" } } catch { Fail "T8 bob_patchAfterDisconnect: $_" } } # --------------------------------------------------------------------------- # 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 }