Files
story/scripts/smoke-device-telemetry.ps1

125 lines
6.7 KiB
PowerShell

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$projectRoot = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $projectRoot
$apiBaseUrl = 'http://localhost:58080'
$deviceInternalId = '95000000-0000-0000-0000-000000000501'
$deviceId = '95000000-0000-0000-0000-000000000511'
$commandInternalId = '95000000-0000-0000-0000-000000000601'
$commandId = '95000000-0000-0000-0000-000000000611'
$deviceSecret = [Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
function Assert-Equal([object]$Actual, [object]$Expected, [string]$Message) {
if ([string]$Actual -ne [string]$Expected) {
throw "$Message。实际值:$Actual;预期值:$Expected"
}
}
function Invoke-Database([string]$Sql) {
$result = docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -t -A -c $Sql
if ($LASTEXITCODE -ne 0) { throw '数据库操作失败。' }
return ($result | Out-String).Trim()
}
function Invoke-SignedTelemetry([string]$Body, [string]$IdempotencyKey) {
$path = '/device/v2/telemetry:batch'
$bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body)
$digest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bodyBytes)).ToLowerInvariant()
$timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
$nonce = [Guid]::NewGuid().ToString('N')
$canonical = "POST`n$path`n$timestamp`n$nonce`n$digest"
$hmac = [Security.Cryptography.HMACSHA256]::new($deviceSecret)
try {
$signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+', '-').Replace('/', '_')
} finally {
$hmac.Dispose()
}
return Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($apiBaseUrl + $path) -ContentType 'application/json' -Body $Body -Headers @{
'X-Device-Id' = $deviceId
'X-Device-Timestamp' = $timestamp
'X-Device-Nonce' = $nonce
'X-Device-Content-SHA256' = $digest
'X-Device-Signature' = $signature
'Idempotency-Key' = $IdempotencyKey
}
}
function Remove-TelemetryFixture {
$cleanup = @"
BEGIN;
DELETE FROM device_telemetry_event WHERE device_id='$deviceInternalId'::uuid;
DELETE FROM idempotency_record WHERE actor_key='device:$deviceId';
DELETE FROM device_presence_state WHERE device_id='$deviceInternalId'::uuid;
DELETE FROM device_command WHERE device_id='$deviceInternalId'::uuid;
DELETE FROM device_twin WHERE device_id='$deviceInternalId'::uuid;
DELETE FROM device WHERE id='$deviceInternalId'::uuid;
COMMIT;
"@
Invoke-Database $cleanup | Out-Null
}
try {
Remove-TelemetryFixture
$fixture = @"
BEGIN;
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref)
VALUES('$deviceInternalId'::uuid,'$deviceId'::uuid,'10000000-0000-0000-0000-000000000001',repeat('6',64),'telemetry-smoke','registered','local-dev://simulated-terminal-1');
INSERT INTO device_twin(device_id,tenant_id,desired_revision,reported_revision)
VALUES('$deviceInternalId'::uuid,'10000000-0000-0000-0000-000000000001',2,1);
INSERT INTO device_command(id,public_id,tenant_id,device_id,command_type,payload,status,requested_by,idempotency_key,request_digest,expires_at,delivery_attempts)
VALUES('$commandInternalId'::uuid,'$commandId'::uuid,'10000000-0000-0000-0000-000000000001','$deviceInternalId'::uuid,'diagnostics','{}','DELIVERED','10000000-0000-0000-0000-000000000101','telemetry-smoke',repeat('5',64),now()+interval '1 hour',1);
COMMIT;
"@
Invoke-Database $fixture | Out-Null
$baseTime = [DateTime]::UtcNow
$bodyObject = @{ events = @(
@{ event_key='smoke-status-1'; sequence_no=1; occurred_at=$baseTime.ToString('o'); event_type='status'; status=@{
firmware_version='telemetry-smoke-1.0.0'; reported_config_revision=1; network=@{type='wifi';rssi=-48};
rtc=@{joined=$false;uplink=$false;downlink=$false}; recording_queue=@{pending_segments=3;failed_segments=0}
}},
@{ event_key='smoke-error-2'; sequence_no=2; occurred_at=$baseTime.AddSeconds(1).ToString('o'); event_type='error'; error=@{code='RTC_RECONNECTING';severity='warning'} },
@{ event_key='smoke-command-3'; sequence_no=3; occurred_at=$baseTime.AddSeconds(2).ToString('o'); event_type='command_result'; command_result=@{
command_id=$commandId; status='SUCCEEDED'; result_summary=@{result_code='OK'}
}}
) }
$body = $bodyObject | ConvertTo-Json -Compress -Depth 8
$key = 'smoke-telemetry-' + [Guid]::NewGuid().ToString('N')
$response = Invoke-SignedTelemetry $body $key
Assert-Equal $response.StatusCode 200 '首次遥测批次失败'
$data = ($response.Content | ConvertFrom-Json).data
Assert-Equal $data.accepted_count 3 '首次遥测接收数量不正确'
Assert-Equal $data.duplicate_count 0 '首次遥测不应出现重复'
Assert-Equal $data.actions[0].type 'refresh_config' '设备配置落后时没有返回刷新动作'
$replay = Invoke-SignedTelemetry $body $key
Assert-Equal $replay.Headers['Idempotency-Replayed'][0] 'true' '遥测幂等重放缺少标记'
$duplicate = Invoke-SignedTelemetry $body ('smoke-telemetry-duplicate-' + [Guid]::NewGuid().ToString('N'))
$duplicateData = ($duplicate.Content | ConvertFrom-Json).data
Assert-Equal $duplicateData.accepted_count 0 '事件级重放被错误重复保存'
Assert-Equal $duplicateData.duplicate_count 3 '事件级重放没有全部判重'
$conflictObject = $body | ConvertFrom-Json
$conflictObject.events[1].error.code = 'RTC_FAILED'
try {
Invoke-SignedTelemetry ($conflictObject | ConvertTo-Json -Compress -Depth 8) ('smoke-telemetry-conflict-' + [Guid]::NewGuid().ToString('N')) | Out-Null
throw '同一遥测业务键对应不同内容不应成功。'
} catch {
if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne 409) { throw }
}
$evidence = Invoke-Database @"
SELECT dt.telemetry_sequence || '|' || dt.firmware_version || '|' || dt.last_error_code || '|' || dt.last_error_severity || '|' ||
dc.status || '|' || (SELECT count(*) FROM device_telemetry_event dte WHERE dte.device_id=dt.device_id)
FROM device_twin dt JOIN device_command dc ON dc.device_id=dt.device_id
WHERE dt.device_id='$deviceInternalId'::uuid;
"@
Assert-Equal $evidence '3|telemetry-smoke-1.0.0|RTC_RECONNECTING|warning|SUCCEEDED|3' '设备影子、错误、命令或事件落库结果不正确'
Write-Host '设备遥测冒烟通过:状态、错误、命令结果、批次幂等、事件判重、冲突回滚和配置刷新动作均正确。'
} finally {
Remove-TelemetryFixture
Write-Host '已清理遥测合成设备、命令、事件、在线状态和幂等记录。'
}