88 lines
5.0 KiB
PowerShell
88 lines
5.0 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|||
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
||
|
|
|
||
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||
|
|
Set-Location -LiteralPath $projectRoot
|
||
|
|
$api = 'http://localhost:58080'
|
||
|
|
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
|
||
|
|
$tenantId = '10000000-0000-0000-0000-000000000001'
|
||
|
|
$deviceId = '10000000-0000-0000-0000-000000000501'
|
||
|
|
$ownerId = '10000000-0000-0000-0000-000000000101'
|
||
|
|
$commandPublicId = [Guid]::NewGuid().ToString()
|
||
|
|
$idempotencyKey = 'event-smoke-' + [Guid]::NewGuid().ToString('N')
|
||
|
|
|
||
|
|
function Get-AccessToken([string]$UserName) {
|
||
|
|
return (Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{
|
||
|
|
grant_type = 'password'; client_id = 'voice-platform-web'; username = $UserName; password = 'LocalOnly123!'; scope = 'openid'
|
||
|
|
}).access_token
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-EventStream([string]$Token, [long]$LastEventId) {
|
||
|
|
$previousPreference = $PSNativeCommandUseErrorActionPreference
|
||
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
||
|
|
try {
|
||
|
|
return (& curl.exe -sS -N --max-time 2 -H "Accept: text/event-stream" -H "Authorization: Bearer $Token" -H "Last-Event-ID: $LastEventId" "$api/api/v1/events" 2>$null | Out-String)
|
||
|
|
} finally {
|
||
|
|
$PSNativeCommandUseErrorActionPreference = $previousPreference
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$ownerToken = Get-AccessToken 'owner'
|
||
|
|
$editorToken = Get-AccessToken 'editor'
|
||
|
|
$outsiderToken = Get-AccessToken 'outsider'
|
||
|
|
|
||
|
|
try {
|
||
|
|
$insertSql = @"
|
||
|
|
INSERT INTO device_command(public_id,tenant_id,device_id,command_type,payload,status,result_summary,requested_by,idempotency_key,request_digest,expires_at)
|
||
|
|
VALUES('$commandPublicId'::uuid,'$tenantId'::uuid,'$deviceId'::uuid,'diagnostics','{}'::jsonb,'QUEUED','{}'::jsonb,'$ownerId'::uuid,'$idempotencyKey',repeat('a',64),now()+interval '5 minutes');
|
||
|
|
UPDATE device_command SET status='SUCCEEDED',completed_at=now(),version=version+1,updated_at=now() WHERE public_id='$commandPublicId'::uuid;
|
||
|
|
"@
|
||
|
|
$insertSql | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
$rows = docker compose exec -T postgres psql -U voice -d voice_platform -t -A -F '|' -c "SELECT id,event_type,status,resource_public_id FROM web_event WHERE resource_public_id='$commandPublicId'::uuid ORDER BY id" | Out-String
|
||
|
|
$events = @($rows.Trim() -split "`r?`n")
|
||
|
|
if ($events.Count -ne 2) { throw "Expected two command events, received $($events.Count): $rows" }
|
||
|
|
$first = $events[0] -split '\|'
|
||
|
|
$second = $events[1] -split '\|'
|
||
|
|
if ($first[1] -ne 'device.command.updated' -or $first[2] -ne 'QUEUED' -or $second[2] -ne 'SUCCEEDED') { throw "Trigger event sequence was unexpected: $rows" }
|
||
|
|
|
||
|
|
$ownerStream = Get-EventStream $ownerToken ([long]$first[0])
|
||
|
|
if ($ownerStream -notmatch "id: $($second[0])" -or $ownerStream -notmatch 'event: device.command.updated' -or $ownerStream -notmatch '"status":"SUCCEEDED"') { throw "Owner did not receive the missing event: $ownerStream" }
|
||
|
|
if ($ownerStream -match 'payload|result_summary|idempotency|requested_by') { throw "Sensitive command fields leaked into SSE: $ownerStream" }
|
||
|
|
|
||
|
|
$editorStream = Get-EventStream $editorToken ([long]$first[0])
|
||
|
|
if ($editorStream -notmatch "id: $($second[0])") { throw 'Active member did not receive the household event' }
|
||
|
|
|
||
|
|
$replayed = Get-EventStream $ownerToken ([long]$second[0])
|
||
|
|
if ($replayed -match 'data:') { throw "Reconnect replayed an already processed event: $replayed" }
|
||
|
|
|
||
|
|
try {
|
||
|
|
Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/events" -Headers @{ Authorization = "Bearer $outsiderToken"; 'Last-Event-ID' = $first[0] } | Out-Null
|
||
|
|
throw 'Cross-tenant event cursor was accepted'
|
||
|
|
} catch {
|
||
|
|
if ($_.Exception.Response.StatusCode.value__ -ne 410) { throw }
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/events" -Headers @{ Authorization = "Bearer $ownerToken"; 'Last-Event-ID' = 'not-a-number' } | Out-Null
|
||
|
|
throw 'Malformed event cursor was accepted'
|
||
|
|
} catch {
|
||
|
|
if ($_.Exception.Response.StatusCode.value__ -ne 400) { throw }
|
||
|
|
}
|
||
|
|
"UPDATE web_event SET expires_at=now()-interval '1 second' WHERE id=$($first[0]);" | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
try {
|
||
|
|
Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/events" -Headers @{ Authorization = "Bearer $ownerToken"; 'Last-Event-ID' = $first[0] } | Out-Null
|
||
|
|
throw 'Expired event cursor was accepted'
|
||
|
|
} catch {
|
||
|
|
if ($_.Exception.Response.StatusCode.value__ -ne 410) { throw }
|
||
|
|
}
|
||
|
|
Write-Host "SSE transaction event, resume cursor, duplicate suppression and household isolation smoke passed: $commandPublicId"
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
$cleanupSql = @"
|
||
|
|
BEGIN;
|
||
|
|
DELETE FROM web_event WHERE resource_public_id='$commandPublicId'::uuid;
|
||
|
|
DELETE FROM device_command WHERE public_id='$commandPublicId'::uuid;
|
||
|
|
COMMIT;
|
||
|
|
"@
|
||
|
|
$cleanupSql | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q | Out-Null
|
||
|
|
}
|