105 lines
6.0 KiB
PowerShell
105 lines
6.0 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|||
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
||
|
|
|
||
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||
|
|
Set-Location -LiteralPath $projectRoot
|
||
|
|
$apiBase = 'http://localhost:58080'
|
||
|
|
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
|
||
|
|
$deviceId = '10000000-0000-0000-0000-000000000511'
|
||
|
|
$originalTwin = $null
|
||
|
|
$patchRequestId = $null
|
||
|
|
$claimBindingId = $null
|
||
|
|
$claimRequestId = $null
|
||
|
|
|
||
|
|
function Get-LocalAccessToken([string]$Username) {
|
||
|
|
$response = 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'
|
||
|
|
}
|
||
|
|
return $response.access_token
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-JSONRequest([string]$Method, [string]$Uri, [string]$Token, [hashtable]$Headers = @{}, [object]$Body = $null) {
|
||
|
|
$requestHeaders = @{ Authorization = "Bearer $Token" }
|
||
|
|
foreach ($key in $Headers.Keys) { $requestHeaders[$key] = $Headers[$key] }
|
||
|
|
$arguments = @{ Method = $Method; Uri = $Uri; Headers = $requestHeaders }
|
||
|
|
if ($null -ne $Body) {
|
||
|
|
$arguments.ContentType = 'application/json; charset=utf-8'
|
||
|
|
$arguments.Body = $Body | ConvertTo-Json -Depth 12 -Compress
|
||
|
|
}
|
||
|
|
return Invoke-WebRequest @arguments
|
||
|
|
}
|
||
|
|
|
||
|
|
function Assert-Status([scriptblock]$Operation, [int]$ExpectedStatus) {
|
||
|
|
try { & $Operation | Out-Null; throw "请求不应成功,预期HTTP $ExpectedStatus。" }
|
||
|
|
catch { if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne $ExpectedStatus) { throw } }
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$ownerToken = Get-LocalAccessToken 'owner'
|
||
|
|
$editorToken = Get-LocalAccessToken 'editor'
|
||
|
|
$outsiderToken = Get-LocalAccessToken 'outsider'
|
||
|
|
|
||
|
|
$devices = (Invoke-JSONRequest Get "$apiBase/api/v1/devices" $ownerToken).Content | ConvertFrom-Json
|
||
|
|
if ($deviceId -notin @($devices.data.items.id)) { throw '所有者设备列表未包含合成设备。' }
|
||
|
|
Assert-Status { Invoke-JSONRequest Get "$apiBase/api/v1/devices/$deviceId" $outsiderToken } 403
|
||
|
|
|
||
|
|
$twinResponse = Invoke-JSONRequest Get "$apiBase/api/v1/devices/$deviceId/twin" $editorToken
|
||
|
|
$originalTwin = $twinResponse.Content | ConvertFrom-Json
|
||
|
|
$originalETag = [string]$twinResponse.Headers.ETag
|
||
|
|
$presenceResponse = Invoke-JSONRequest Get "$apiBase/api/v1/devices/$deviceId/presence" $ownerToken
|
||
|
|
$presence = $presenceResponse.Content | ConvertFrom-Json
|
||
|
|
if ($presence.data.device_id -ne $deviceId -or [string]$presenceResponse.Headers.'Cache-Control' -ne 'no-store') {
|
||
|
|
throw '设备在线状态响应不符合契约。'
|
||
|
|
}
|
||
|
|
|
||
|
|
Assert-Status { Invoke-JSONRequest Patch "$apiBase/api/v1/devices/$deviceId/twin" $editorToken @{ 'If-Match' = $originalETag } @{
|
||
|
|
desired_config = @{ upload = @{ api_token = 'forbidden' } }
|
||
|
|
} } 400
|
||
|
|
|
||
|
|
$patchedResponse = Invoke-JSONRequest Patch "$apiBase/api/v1/devices/$deviceId/twin" $editorToken @{ 'If-Match' = $originalETag } @{
|
||
|
|
desired_config = @{
|
||
|
|
volume = @{ speaker = 60 }
|
||
|
|
interaction = @{ pace = 'normal' }
|
||
|
|
upload = @{ policy = 'wifi_preferred' }
|
||
|
|
policy_refs = @{ retention = 'local-v1' }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$patched = $patchedResponse.Content | ConvertFrom-Json
|
||
|
|
$patchRequestId = $patched.request_id
|
||
|
|
if ($patched.data.desired_revision -ne ($originalTwin.data.desired_revision + 1) -or $patched.data.sync_state -ne 'pending') {
|
||
|
|
throw '设备影子期望版本或同步状态不正确。'
|
||
|
|
}
|
||
|
|
Assert-Status { Invoke-JSONRequest Patch "$apiBase/api/v1/devices/$deviceId/twin" $editorToken @{ 'If-Match' = $originalETag } @{ desired_config = @{} } } 412
|
||
|
|
|
||
|
|
$claimBody = @{
|
||
|
|
claim_code = 'LOCAL-CLAIM-0001'
|
||
|
|
household_id = '10000000-0000-0000-0000-000000000211'
|
||
|
|
subject_id = '10000000-0000-0000-0000-000000000411'
|
||
|
|
}
|
||
|
|
Assert-Status { Invoke-JSONRequest Post "$apiBase/api/v1/device-claims" $outsiderToken @{} $claimBody } 403
|
||
|
|
$claimResponse = Invoke-JSONRequest Post "$apiBase/api/v1/device-claims" $ownerToken @{} $claimBody
|
||
|
|
$claim = $claimResponse.Content | ConvertFrom-Json
|
||
|
|
$claimBindingId = $claim.data.binding_id
|
||
|
|
$claimRequestId = $claim.request_id
|
||
|
|
$claimReplay = Invoke-JSONRequest Post "$apiBase/api/v1/device-claims" $ownerToken @{} $claimBody
|
||
|
|
$claimReplayBody = $claimReplay.Content | ConvertFrom-Json
|
||
|
|
if ([string]$claimReplay.Headers.'Idempotency-Replayed' -ne 'true' -or $claimReplayBody.data.binding_id -ne $claimBindingId) {
|
||
|
|
throw '一次性设备绑定码安全重试结果不正确。'
|
||
|
|
}
|
||
|
|
Write-Host "设备管理烟测通过:设备 $deviceId,在线状态来源 $($presence.data.source),绑定 $claimBindingId。"
|
||
|
|
} finally {
|
||
|
|
if (-not [string]::IsNullOrWhiteSpace($claimBindingId)) {
|
||
|
|
$deleteClaimAudit = if ([string]::IsNullOrWhiteSpace($claimRequestId)) { '' } else { "DELETE FROM audit_event WHERE request_id='$claimRequestId';" }
|
||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c `
|
||
|
|
"BEGIN; UPDATE device_claim SET used_at=NULL,used_binding_id=NULL WHERE public_id='10000000-0000-0000-0000-000000000911'::uuid; DELETE FROM device_binding WHERE public_id='$claimBindingId'::uuid; $deleteClaimAudit COMMIT;" | Out-Null
|
||
|
|
}
|
||
|
|
if ($null -ne $originalTwin) {
|
||
|
|
$desiredJSON = ($originalTwin.data.desired_config | ConvertTo-Json -Depth 12 -Compress).Replace("'", "''")
|
||
|
|
$updatedAt = ([DateTimeOffset]$originalTwin.data.updated_at).ToString('o')
|
||
|
|
$deleteAudit = if ([string]::IsNullOrWhiteSpace($patchRequestId)) { '' } else { "DELETE FROM audit_event WHERE request_id='$patchRequestId';" }
|
||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c `
|
||
|
|
"BEGIN; UPDATE device_twin SET desired_revision=$($originalTwin.data.desired_revision),desired_config='$desiredJSON'::jsonb,updated_at='$updatedAt'::timestamptz WHERE device_id=(SELECT id FROM device WHERE public_id='$deviceId'::uuid); $deleteAudit COMMIT;" | Out-Null
|
||
|
|
}
|
||
|
|
}
|