$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' $privacyRequestId = $null 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 Remove-PrivacyRequest([string]$PublicId) { if ($PublicId -notmatch '^[0-9a-fA-F-]{36}$') { throw '拒绝清理格式不正确的隐私请求编号。' } $sql = @" BEGIN; CREATE TEMP TABLE smoke_privacy_cleanup AS SELECT id,public_id FROM privacy_request WHERE public_id='$PublicId'::uuid; DELETE FROM outbox_event WHERE aggregate_id IN (SELECT id FROM smoke_privacy_cleanup); DELETE FROM audit_event WHERE resource_id IN (SELECT public_id FROM smoke_privacy_cleanup); DELETE FROM privacy_request WHERE id IN (SELECT id FROM smoke_privacy_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 { $health = Invoke-WebRequest -UseBasicParsing -Uri "$apiBase/health/ready" Assert-Equal $health.StatusCode 200 'API就绪检查失败' $ownerToken = Get-LocalAccessToken 'owner' $editorToken = Get-LocalAccessToken 'editor' $key = 'smoke-privacy-' + [Guid]::NewGuid().ToString('N') $body = @{ request_type = 'ACCESS'; scope = @{ categories = @('profile', 'conversations', 'memories') } } | ConvertTo-Json -Compress -Depth 4 $headers = @{ Authorization = "Bearer $ownerToken"; 'Idempotency-Key' = $key } $created = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/privacy-requests" -Headers $headers -ContentType 'application/json' -Body $body Assert-Equal $created.StatusCode 202 '隐私访问请求未进入后台队列' $privacyRequestId = [string](($created.Content | ConvertFrom-Json).data.id) if ($privacyRequestId -notmatch '^[0-9a-fA-F-]{36}$') { throw '隐私请求编号不正确。' } $replay = Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/privacy-requests" -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) $privacyRequestId '幂等重放创建了第二个请求' $conflictingBody = @{ request_type = 'ACCESS'; scope = @{ categories = @('profile') } } | ConvertTo-Json -Compress -Depth 4 Invoke-ExpectedStatus { Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/privacy-requests" -Headers $headers -ContentType 'application/json' -Body $conflictingBody } 409 Invoke-ExpectedStatus { Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/api/v1/subjects/$subjectId/privacy-requests" -Headers @{ Authorization = "Bearer $editorToken"; 'Idempotency-Key' = ('editor-denied-' + [Guid]::NewGuid().ToString('N')) } -ContentType 'application/json' -Body $body } 403 $deadline = [DateTime]::UtcNow.AddSeconds(20) do { Start-Sleep -Milliseconds 500 $detailResponse = Invoke-WebRequest -UseBasicParsing -Uri "$apiBase/api/v1/privacy-requests/$privacyRequestId" -Headers @{ Authorization = "Bearer $ownerToken" } $detail = ($detailResponse.Content | ConvertFrom-Json).data } while ($detail.status -notin @('COMPLETED', 'PARTIAL_FAILED') -and [DateTime]::UtcNow -lt $deadline) Assert-Equal $detail.status 'COMPLETED' '隐私数据盘点没有完成' Assert-Equal $detail.subtasks.Count 1 '隐私请求子任务数量不正确' Assert-Equal $detail.subtasks[0].status 'COMPLETED' '隐私数据库盘点子任务未完成' Assert-Equal $detail.result_summary.content_included $false '隐私盘点结果不应包含正文' Assert-Equal @($detail.result_summary.inventory_counts.PSObject.Properties).Count 3 '隐私盘点没有按请求范围过滤' if ($detail.result_summary.inventory_counts.profile -lt 1) { throw '隐私盘点没有返回长者档案记录。' } $list = (Invoke-WebRequest -UseBasicParsing -Uri "$apiBase/api/v1/subjects/$subjectId/privacy-requests" -Headers @{ Authorization = "Bearer $ownerToken" }).Content | ConvertFrom-Json if (-not ($list.data.items | Where-Object id -eq $privacyRequestId)) { throw '隐私请求列表没有返回刚创建的请求。' } Write-Host "隐私请求端到端冒烟通过:$privacyRequestId" Write-Host '权限拒绝、幂等重放/冲突、后台子任务、范围过滤和无正文盘点验证通过。' } finally { if ($privacyRequestId) { Remove-PrivacyRequest $privacyRequestId Write-Host "已清理本次隐私请求冒烟数据:$privacyRequestId" } }