- Client: Token als 'Authorization: Bearer' Header statt ?token= Query-Parameter senden - Server: Token aus Authorization-Header statt Query-Parameter lesen - Tests: Alle 8 WebSocket-Tests auf Header-Auth umgestellt - Integration-Tests: WebSocket-Verbindung mit Header aktualisiert Closes #97
884 lines
41 KiB
PowerShell
884 lines
41 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Bollwerk 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: https://bollwerk.online
|
||
.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 = "https://bollwerk.online",
|
||
[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 }
|
||
# PS5.1 sendet String-Body mit Windows-Systemkodierung statt UTF-8.
|
||
# Alle Non-ASCII-Zeichen als \uXXXX escapen → Body ist ASCII-sicher.
|
||
if ($bodyJson) {
|
||
$sb = [System.Text.StringBuilder]::new($bodyJson.Length * 2)
|
||
foreach ($ch in $bodyJson.ToCharArray()) {
|
||
if ([int]$ch -gt 127) { [void]$sb.Append('\u{0:x4}' -f [int]$ch) }
|
||
else { [void]$sb.Append($ch) }
|
||
}
|
||
$bodyJson = $sb.ToString()
|
||
}
|
||
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
|
||
$ws.Options.SetRequestHeader("Authorization", "Bearer $token")
|
||
$uri = [Uri]"$WsUrl/ws/sync"
|
||
$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
|
||
kcalPerUnit = $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
|
||
kcalPerUnit = $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"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Szenario 6a: UTF-8-Unterstuetzung (Umlaute und Emojis im Chat)
|
||
# ---------------------------------------------------------------------------
|
||
Section "Szenario 6a: UTF-8 & Emoji im Chat"
|
||
|
||
if ($aliceTokens -and $bobTokens) {
|
||
$bobId = $bobTokens.userId
|
||
$aliceId = $aliceTokens.userId
|
||
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
||
|
||
# ---- Testfall 1: Nachricht mit Umlauten senden und empfangen ----
|
||
try {
|
||
$umlautBody = "Schöne Grüße aus München – äöüÄÖÜß"
|
||
$umlautMsg = Invoke-Api -Method POST -Path "/api/messages" -Token $aliceTokens.accessToken -Body @{
|
||
receiverId = $bobId
|
||
body = $umlautBody
|
||
sentAt = $now + 1000
|
||
}
|
||
if ($umlautMsg.body -eq $umlautBody) {
|
||
Pass "UTF-8: Umlaut-Nachricht korrekt gespeichert und zurueckgegeben (äöüÄÖÜß)"
|
||
}
|
||
else {
|
||
Fail "UTF-8: Umlaut-Nachricht veraendert: erwartet='$umlautBody', erhalten='$($umlautMsg.body)'"
|
||
}
|
||
}
|
||
catch { Fail "UTF-8: Umlaut-Nachricht senden - $_" }
|
||
|
||
# ---- Testfall 2: Nachricht mit Emojis senden und empfangen ----
|
||
try {
|
||
$emojiBody = "Guten Morgen! 😀🎉❤️👍"
|
||
$emojiMsg = Invoke-Api -Method POST -Path "/api/messages" -Token $aliceTokens.accessToken -Body @{
|
||
receiverId = $bobId
|
||
body = $emojiBody
|
||
sentAt = $now + 2000
|
||
}
|
||
if ($emojiMsg.body -eq $emojiBody) {
|
||
Pass "UTF-8: Emoji-Nachricht korrekt gespeichert und zurueckgegeben (😀🎉❤️👍)"
|
||
}
|
||
else {
|
||
Fail "UTF-8: Emoji-Nachricht veraendert: erwartet='$emojiBody', erhalten='$($emojiMsg.body)'"
|
||
}
|
||
}
|
||
catch { Fail "UTF-8: Emoji-Nachricht senden - $_" }
|
||
|
||
# ---- Testfall 3: Gemischte Nachricht (Umlaut + Emoji + ASCII) ----
|
||
try {
|
||
$mixedBody = "Schöner Tag 😀! Grüße & Küsse 💋"
|
||
$mixedMsg = Invoke-Api -Method POST -Path "/api/messages" -Token $aliceTokens.accessToken -Body @{
|
||
receiverId = $bobId
|
||
body = $mixedBody
|
||
sentAt = $now + 3000
|
||
}
|
||
if ($mixedMsg.body -eq $mixedBody) {
|
||
Pass "UTF-8: Gemischte Nachricht (Umlaut+Emoji+ASCII) korrekt"
|
||
}
|
||
else {
|
||
Fail "UTF-8: Gemischte Nachricht veraendert: erwartet='$mixedBody', erhalten='$($mixedMsg.body)'"
|
||
}
|
||
}
|
||
catch { Fail "UTF-8: Gemischte Nachricht senden - $_" }
|
||
|
||
# ---- Testfall 4: Konversation mit Umlauten/Emojis abrufen ----
|
||
try {
|
||
$conv = Invoke-Api -Method GET -Path "/api/messages/$bobId" -Token $aliceTokens.accessToken
|
||
$umlautFound = $conv | Where-Object { $_.body -like "*äöüÄÖÜß*" }
|
||
$emojiFound = $conv | Where-Object { $_.body -like "*😀*" }
|
||
if ($umlautFound -and $emojiFound) {
|
||
Pass "UTF-8: Konversationsverlauf enthaelt Umlaute und Emojis korrekt"
|
||
}
|
||
else {
|
||
Fail "UTF-8: Konversationsverlauf: umlaut=$([bool]$umlautFound), emoji=$([bool]$emojiFound)"
|
||
}
|
||
}
|
||
catch { Fail "UTF-8: Konversation abrufen - $_" }
|
||
|
||
# ---- Testfall 5: WebSocket-Delivery mit UTF-8 ----
|
||
try {
|
||
$bobWsUtf8 = Open-WebSocket -token $bobTokens.accessToken
|
||
Start-Sleep -Milliseconds 500
|
||
|
||
$wsBody = "Hallo Böb! 🚀 Schöne Grüße"
|
||
Invoke-Api -Method POST -Path "/api/messages" -Token $aliceTokens.accessToken -Body @{
|
||
receiverId = $bobId
|
||
body = $wsBody
|
||
sentAt = $now + 4000
|
||
} | Out-Null
|
||
|
||
Start-Sleep -Milliseconds 500
|
||
$wsReceived = Receive-WsMessages -ws $bobWsUtf8 -waitSeconds 4
|
||
Close-WebSocket -ws $bobWsUtf8
|
||
|
||
$wsMsg = $wsReceived | Where-Object { $_.type -eq "new_message" -and $_.body -eq $wsBody }
|
||
if ($wsMsg) {
|
||
Pass "UTF-8: WebSocket-Delivery mit Umlauten und Emojis korrekt"
|
||
}
|
||
else {
|
||
$bodies = ($wsReceived | Where-Object { $_.type -eq "new_message" } | Select-Object -ExpandProperty body) -join "; "
|
||
Fail "UTF-8: WebSocket-Body veraendert. Bodies: $bodies"
|
||
}
|
||
}
|
||
catch { Fail "UTF-8: WebSocket UTF-8 Delivery - $_" }
|
||
}
|
||
|
||
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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
|
||
@{ id = $seifenId; name = "Seife"; categoryId = 12; quantity = 4.0; unit = "Stueck"; unitPrice = 1.5; kcalPerUnit = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
|
||
@{ id = $muesliId; name = "Muesliegel"; categoryId = 10; quantity = 20.0; unit = "Stueck"; unitPrice = 0.8; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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
|
||
kcalPerUnit = $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; kcalPerUnit = $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
|
||
kcalPerUnit = $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
|
||
kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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; kcalPerUnit = $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 }
|