Files
story/scripts/smoke-export.ps1
T

112 lines
7.6 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'
$subjectId = '10000000-0000-0000-0000-000000000411'
$exportId = $null
$objectRef = $null
$tempDirectory = Join-Path ([IO.Path]::GetTempPath()) ('voice-export-smoke-' + [Guid]::NewGuid().ToString('N'))
function Get-LocalAccessToken([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 Assert-Equal([object]$Actual,[object]$Expected,[string]$Message) {
if ([string]$Actual -ne [string]$Expected) { throw "$Message。实际值:$Actual;预期值:$Expected" }
}
function Invoke-ExpectedStatus([scriptblock]$Action,[int]$ExpectedStatus) {
try { $response=& $Action; Assert-Equal $response.StatusCode $ExpectedStatus 'HTTP状态不正确' }
catch { if ($_.Exception.Response.StatusCode.value__ -ne $ExpectedStatus) { throw } }
}
function Invoke-ScalarQuery([string]$Sql) {
$value=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 ($value|Out-String).Trim()
}
function Remove-SmokeExport([string]$PublicId) {
if ($PublicId -notmatch '^[0-9a-fA-F-]{36}$') { throw '拒绝清理格式不正确的导出任务。' }
$sql=@"
BEGIN;
CREATE TEMP TABLE smoke_export_cleanup AS SELECT id,public_id FROM export_task WHERE public_id='$PublicId'::uuid;
DELETE FROM outbox_event WHERE aggregate_id IN (SELECT id FROM smoke_export_cleanup);
DELETE FROM audit_event WHERE resource_id IN (SELECT public_id FROM smoke_export_cleanup);
DELETE FROM export_task WHERE id IN (SELECT id FROM smoke_export_cleanup);
COMMIT;
"@
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c $sql | Out-Null
if ($LASTEXITCODE -ne 0) { throw '导出任务清理失败。' }
}
try {
New-Item -ItemType Directory -Path $tempDirectory | Out-Null
Assert-Equal (Invoke-WebRequest -UseBasicParsing -Uri "$apiBase/health/ready").StatusCode 200 'API就绪检查失败'
$ownerToken=Get-LocalAccessToken 'owner'
$editorToken=Get-LocalAccessToken 'editor'
$key='smoke-export-'+[Guid]::NewGuid().ToString('N')
$body=@{scope=@{categories=@('profile','consents','device_bindings')}}|ConvertTo-Json -Compress -Depth 4
$headers=@{Authorization="Bearer $ownerToken";'Idempotency-Key'=$key}
$created=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/exports" -Headers $headers -ContentType 'application/json' -Body $body
Assert-Equal $created.StatusCode 202 '导出任务未进入后台队列'
$exportId=[string](($created.Content|ConvertFrom-Json).data.id)
if ($created.Content -match 'object_ref') { throw 'API响应泄露了对象存储内部路径。' }
$replay=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/exports" -Headers $headers -ContentType 'application/json' -Body $body
Assert-Equal $replay.StatusCode 200 '导出幂等重放状态不正确'
Assert-Equal $replay.Headers['Idempotency-Replayed'][0] 'true' '导出幂等重放缺少标记'
Assert-Equal (($replay.Content|ConvertFrom-Json).data.id) $exportId '导出幂等重放创建了新任务'
Invoke-ExpectedStatus { Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/exports" -Headers @{Authorization="Bearer $editorToken";'Idempotency-Key'=('editor-export-'+[Guid]::NewGuid().ToString('N'))} -ContentType 'application/json' -Body $body } 403
$deadline=[DateTime]::UtcNow.AddSeconds(25)
do { Start-Sleep -Milliseconds 500; $response=Invoke-WebRequest -UseBasicParsing -Uri "$apiBase/api/v1/exports/$exportId" -Headers @{Authorization="Bearer $ownerToken"}; $task=($response.Content|ConvertFrom-Json).data } while ($task.status -notin @('READY','FAILED','EXPIRED') -and [DateTime]::UtcNow -lt $deadline)
Assert-Equal $task.status 'READY' '私有导出文件未就绪'
if (-not $task.download_url -or $task.download_url -notmatch '^http://localhost:59000/') { throw '未返回浏览器可用的MinIO短期下载地址。' }
if ($response.Headers['Cache-Control'][0] -ne 'no-store') { throw '导出详情响应未禁止缓存。' }
$objectRef=Invoke-ScalarQuery "SELECT object_ref FROM export_task WHERE public_id='$exportId'::uuid"
if ($objectRef -notmatch '^tenants/[0-9a-f]{32}/exports/[0-9a-f-]{36}/v1\.json$') { throw "对象路径未使用租户摘要边界:$objectRef" }
Invoke-ExpectedStatus { Invoke-WebRequest -UseBasicParsing -Uri "http://localhost:59000/private-exports/$objectRef" } 403
$downloadPath=Join-Path $tempDirectory 'export.json'
Invoke-WebRequest -UseBasicParsing -Uri $task.download_url -OutFile $downloadPath
$bytes=[IO.File]::ReadAllBytes($downloadPath)
$digest=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
Assert-Equal $digest $task.content_digest '下载文件校验值不匹配'
$document=Get-Content -LiteralPath $downloadPath -Raw -Encoding UTF8 | ConvertFrom-Json
Assert-Equal $document.schema_version 'subject-export-v1' '导出文件模式版本不正确'
Assert-Equal @($document.data.PSObject.Properties).Count 3 '导出文件没有按请求范围生成'
Assert-Equal $document.data.profile.id $subjectId '导出文件的长者边界不正确'
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -c "UPDATE export_task SET expires_at=now()-interval '1 second' WHERE public_id='$exportId'::uuid" | Out-Null
$deadline=[DateTime]::UtcNow.AddSeconds(15)
do { Start-Sleep -Milliseconds 500; $status=Invoke-ScalarQuery "SELECT status FROM export_task WHERE public_id='$exportId'::uuid" } while ($status -ne 'EXPIRED' -and [DateTime]::UtcNow -lt $deadline)
Assert-Equal $status 'EXPIRED' '过期扫描器没有收敛导出任务'
docker compose exec -T minio mc alias set smoke http://localhost:9000 local_minio local_minio_password | Out-Null
$nativePreference=$PSNativeCommandUseErrorActionPreference
$PSNativeCommandUseErrorActionPreference=$false
docker compose exec -T minio mc stat "smoke/private-exports/$objectRef" 2>$null | Out-Null
$statExitCode=$LASTEXITCODE
$PSNativeCommandUseErrorActionPreference=$nativePreference
if ($statExitCode -eq 0) { throw '导出过期后对象仍存在。' }
Write-Host "私有导出端到端冒烟通过:$exportId"
Write-Host '私有桶、范围限定、摘要校验、短期签名、幂等/权限和过期删除均已验证。'
} finally {
if ($objectRef) {
$nativePreference=$PSNativeCommandUseErrorActionPreference
$PSNativeCommandUseErrorActionPreference=$false
docker compose exec -T minio mc alias set smoke http://localhost:9000 local_minio local_minio_password 2>$null | Out-Null
docker compose exec -T minio mc rm --force "smoke/private-exports/$objectRef" 2>$null | Out-Null
$PSNativeCommandUseErrorActionPreference=$nativePreference
}
if ($exportId) { Remove-SmokeExport $exportId; Write-Host "已清理本次导出冒烟数据:$exportId" }
if (Test-Path -LiteralPath $tempDirectory) { Remove-Item -LiteralPath $tempDirectory -Recurse -Force }
}