Files
story/scripts/smoke-members.ps1
T

95 lines
7.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'
$tenantId = '10000000-0000-0000-0000-000000000010'
$householdId = $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 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 10 -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 } }
}
function Convert-Response($Response) { return $Response.Content | ConvertFrom-Json }
try {
$ownerToken = Get-LocalAccessToken 'owner'
$editorToken = Get-LocalAccessToken 'editor'
$outsiderToken = Get-LocalAccessToken 'outsider'
$createdResponse = Invoke-JSONRequest Post "$apiBase/api/v1/households" $ownerToken @{ 'X-Tenant-Id' = $tenantId } @{ name = '成员管理纵向烟测家庭' }
$household = (Convert-Response $createdResponse).data
$householdId = $household.id
$householdETag = [string]($createdResponse.Headers.ETag | Select-Object -First 1)
Assert-Status { Invoke-JSONRequest Get "$apiBase/api/v1/households/$householdId/members" $outsiderToken } 403
Assert-Status { Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/invitations" $editorToken @{} @{ invitee_hint = '禁止'; role = 'viewer' } } 403
$invitationResponse = Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/invitations" $ownerToken @{} @{
invitee_hint = '跨客户组织受邀账号'; role = 'editor'; expires_in_hours = 2
}
$invitation = (Convert-Response $invitationResponse).data
if ([string]::IsNullOrWhiteSpace($invitation.token)) { throw '创建邀请未返回一次性凭据。' }
$storedDigest = docker compose exec -T postgres psql -U voice -d voice_platform -Atc "SELECT token_digest FROM member_invitation WHERE public_id='$($invitation.id)'::uuid;"
if ($storedDigest.Trim() -eq $invitation.token -or $storedDigest.Trim().Length -ne 64) { throw '数据库未仅保存邀请SHA-256摘要。' }
$accepted = Convert-Response (Invoke-JSONRequest Post "$apiBase/api/v1/member-invitations/accept" $outsiderToken @{} @{ token = $invitation.token })
if ($accepted.data.role -ne 'editor') { throw '接受邀请后的角色不正确。' }
Assert-Status { Invoke-JSONRequest Post "$apiBase/api/v1/member-invitations/accept" $outsiderToken @{} @{ token = $invitation.token } } 409
$members = (Convert-Response (Invoke-JSONRequest Get "$apiBase/api/v1/households/$householdId/members" $ownerToken)).data.items
$ownerMember = $members | Where-Object is_owner -eq $true
$editorMember = $members | Where-Object user_id -eq $accepted.data.user_id
if ($members.Count -ne 2) { throw "成员数量不正确:$($members.Count)" }
Assert-Status { Invoke-JSONRequest Delete "$apiBase/api/v1/households/$householdId/members/$($ownerMember.id)" $ownerToken @{ 'If-Match' = '"v1"' } } 409
$roleResponse = Invoke-JSONRequest Patch "$apiBase/api/v1/households/$householdId/members/$($editorMember.id)" $ownerToken @{ 'If-Match' = '"v1"' } @{ role = 'admin' }
$editorMember = (Convert-Response $roleResponse).data
Assert-Status { Invoke-JSONRequest Patch "$apiBase/api/v1/households/$householdId/members/$($editorMember.id)" $ownerToken @{ 'If-Match' = '"v1"' } @{ role = 'viewer' } } 412
$revokable = (Convert-Response (Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/invitations" $outsiderToken @{} @{ invitee_hint = '待撤销'; role = 'viewer' })).data
$revoked = (Convert-Response (Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/invitations/$($revokable.id)/revoke" $outsiderToken)).data
if ($revoked.status -ne 'revoked') { throw '管理员撤销邀请失败。' }
$transferResponse = Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/ownership" $ownerToken @{ 'If-Match' = $householdETag } @{ member_id = $editorMember.id }
$householdETag = [string]($transferResponse.Headers.ETag | Select-Object -First 1)
$members = (Convert-Response (Invoke-JSONRequest Get "$apiBase/api/v1/households/$householdId/members" $outsiderToken)).data.items
$formerOwner = $members | Where-Object user_id -eq $ownerMember.user_id
$newOwner = $members | Where-Object user_id -eq $editorMember.user_id
if (-not $newOwner.is_owner -or $formerOwner.role -ne 'admin') { throw '首次所有权转移后的角色不正确。' }
$transferBack = Invoke-JSONRequest Post "$apiBase/api/v1/households/$householdId/ownership" $outsiderToken @{ 'If-Match' = $householdETag } @{ member_id = $formerOwner.id }
$members = (Convert-Response (Invoke-JSONRequest Get "$apiBase/api/v1/households/$householdId/members" $ownerToken)).data.items
$editorMember = $members | Where-Object user_id -eq $editorMember.user_id
$removed = (Convert-Response (Invoke-JSONRequest Delete "$apiBase/api/v1/households/$householdId/members/$($editorMember.id)" $ownerToken @{ 'If-Match' = "`"v$($editorMember.version)`"" })).data
if ($removed.status -ne 'removed') { throw '成员移除状态不正确。' }
Write-Host "成员管理烟测通过:家庭 $householdId,邀请摘要安全、复用拒绝、角色并发、所有权转移与成员移除均通过。"
} finally {
if ($householdId) {
docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U voice -d voice_platform -c `
"BEGIN; DELETE FROM audit_event WHERE request_id LIKE 'req-%' AND (resource_id='$householdId'::uuid OR detail->>'household_id'='$householdId'); DELETE FROM member_invitation WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid); DELETE FROM household_member WHERE household_id=(SELECT id FROM household WHERE public_id='$householdId'::uuid); DELETE FROM household WHERE public_id='$householdId'::uuid; DELETE FROM user_account WHERE tenant_id='10000000-0000-0000-0000-000000000001'::uuid AND oidc_subject='00000000-0000-0000-0000-000000000103'; COMMIT;" | Out-Null
}
}