Files
story/scripts/smoke-sharing.ps1

114 lines
13 KiB
PowerShell

$ErrorActionPreference='Stop'
$PSNativeCommandUseErrorActionPreference=$true
$root=Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $root
$api='http://localhost:58080'
$tokenUrl='http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
$tenantId='10000000-0000-0000-0000-000000000001'
$subjectPublicId='10000000-0000-0000-0000-000000000411'
$editorPublicId='10000000-0000-0000-0000-000000000112'
$outsiderPublicId='20000000-0000-0000-0000-000000000113'
$conversationPublicId=[Guid]::NewGuid().ToString()
$contextId=[Guid]::NewGuid().ToString()
$storyPublicId=[Guid]::NewGuid().ToString()
$providerRef='sharing-smoke-'+[Guid]::NewGuid().ToString('N')
$key1='sharing-smoke-'+[Guid]::NewGuid().ToString('N')
$key2='sharing-smoke-'+[Guid]::NewGuid().ToString('N')
$key3='sharing-smoke-'+[Guid]::NewGuid().ToString('N')
$key4='sharing-smoke-'+[Guid]::NewGuid().ToString('N')
$grantIds=@()
function Token([string]$name){(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{grant_type='password';client_id='voice-platform-web';username=$name;password='LocalOnly123!';scope='openid'}).access_token}
function Expected([scriptblock]$call,[int]$status){try{&$call|Out-Null;throw "Expected HTTP $status"}catch{if($_.Exception.Response.StatusCode.value__-ne$status){throw}}}
try{
$setup=@"
INSERT INTO context_snapshot(id,tenant_id,subject_id,profile_version,consent_snapshot_id,memory_versions,prompt_bundle_version,engine_mode,content_digest,sanitized_summary)
VALUES('$contextId'::uuid,'$tenantId'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,1,'10000000-0000-0000-0000-000000000702'::uuid,'[]'::jsonb,'sharing-smoke','s2s_hybrid',repeat('0',64),'合成分享烟测');
WITH inserted AS (
INSERT INTO conversation(public_id,tenant_id,household_id,subject_id,device_id,consent_snapshot_id,context_snapshot_id,provider_task_ref,engine_mode,retention_mode,status,allocation_expires_at,started_at,ended_at)
VALUES('$conversationPublicId'::uuid,'$tenantId'::uuid,'10000000-0000-0000-0000-000000000201'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,'10000000-0000-0000-0000-000000000501'::uuid,'10000000-0000-0000-0000-000000000702'::uuid,'$contextId'::uuid,'$providerRef','s2s_hybrid','transcript_only','COMPLETED',now()+interval '1 minute',now()-interval '2 minutes',now()) RETURNING id
)
INSERT INTO conversation_turn(tenant_id,subject_id,conversation_id,turn_key,sequence_no,speaker,start_at,end_at,final_text,source_type)
SELECT '$tenantId'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,id,'sharing-turn-1',1,'subject',now()-interval '2 minutes',now()-interval '119 seconds','这是仅用于分享权限验证的合成文字。','provider' FROM inserted;
INSERT INTO story_draft(public_id,tenant_id,subject_id,conversation_id,title,status,confidence,sensitivity,generation_version)
SELECT '$storyPublicId'::uuid,'$tenantId'::uuid,'10000000-0000-0000-0000-000000000401'::uuid,id,'合成分享故事','IN_REVIEW',0.9,'normal',1 FROM conversation WHERE public_id='$conversationPublicId'::uuid;
WITH revision AS (
INSERT INTO story_revision(story_id,revision_no,content,author_type,model_ref,prompt_version)
SELECT id,1,'这是仅用于分享权限验证的合成故事正文。','model','sharing-smoke','v1' FROM story_draft WHERE public_id='$storyPublicId'::uuid RETURNING id,story_id
)
UPDATE story_draft sd SET current_revision_id=revision.id,version=version+1,updated_at=now() FROM revision WHERE sd.id=revision.story_id;
"@
$setup|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null
$owner=Token 'owner';$editor=Token 'editor';$outsider=Token 'outsider'
$body=@{resource_type='STORY';resource_id=$storyPublicId;grantee_user_id=$editorPublicId;permission='VIEW_TEXT';purpose='家庭定向分享烟测';expires_at=[DateTime]::UtcNow.AddMinutes(10).ToString('o');login_required=$true}|ConvertTo-Json -Compress
$created=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key1} -ContentType 'application/json' -Body $body
if($created.StatusCode-ne201){throw 'Sharing grant was not created'}
$grantId=[string](($created.Content|ConvertFrom-Json).data.id);$grantIds+=$grantId
$replay=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key1} -ContentType 'application/json' -Body $body
if($replay.StatusCode-ne200-or[string]$replay.Headers.'Idempotency-Replayed'-ne'true'){throw 'Sharing grant idempotency replay failed'}
$shared=(Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants/$grantId/resource" -Headers @{Authorization="Bearer $editor"}).Content|ConvertFrom-Json
if($shared.data.text-ne'这是仅用于分享权限验证的合成故事正文。'-or$shared.data.permission-ne'VIEW_TEXT'){throw 'Recipient did not receive the shared story'}
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants/$grantId/resource" -Headers @{Authorization="Bearer $owner"}} 403
$listed=(Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants?subject_id=$subjectPublicId" -Headers @{Authorization="Bearer $owner"}).Content|ConvertFrom-Json
if(@($listed.data.items|Where-Object id -eq $grantId).Count-ne1){throw 'Grant was missing from owner history'}
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants?subject_id=$subjectPublicId" -Headers @{Authorization="Bearer $editor"}} 403
$badBody=@{resource_type='STORY';resource_id=$storyPublicId;grantee_user_id=$editorPublicId;permission='LISTEN';purpose='invalid';expires_at=[DateTime]::UtcNow.AddMinutes(10).ToString('o');login_required=$true}|ConvertTo-Json -Compress
Expected {Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=('sharing-invalid-'+[Guid]::NewGuid().ToString('N'))} -ContentType 'application/json' -Body $badBody} 400
$crossBody=@{resource_type='STORY';resource_id=$storyPublicId;grantee_user_id=$outsiderPublicId;permission='VIEW_TEXT';purpose='cross tenant';expires_at=[DateTime]::UtcNow.AddMinutes(10).ToString('o');login_required=$true}|ConvertTo-Json -Compress
Expected {Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=('sharing-cross-'+[Guid]::NewGuid().ToString('N'))} -ContentType 'application/json' -Body $crossBody} 403
Invoke-WebRequest -UseBasicParsing -Method Delete -Uri "$api/api/v1/sharing-grants/$grantId" -Headers @{Authorization="Bearer $owner"}|Out-Null
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants/$grantId/resource" -Headers @{Authorization="Bearer $editor"}} 410
$second=(Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key2} -ContentType 'application/json' -Body $body).Content|ConvertFrom-Json
$expiredId=[string]$second.data.id;$grantIds+=$expiredId
"UPDATE sharing_grant SET granted_at=now()-interval '10 minutes',expires_at=now()-interval '1 minute' WHERE public_id='$expiredId'::uuid;"|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants/$expiredId/resource" -Headers @{Authorization="Bearer $editor"}} 410
$editBody=@{resource_type='STORY';resource_id=$storyPublicId;grantee_user_id=$editorPublicId;permission='EDIT';purpose='共同修订故事';expires_at=[DateTime]::UtcNow.AddMinutes(10).ToString('o');login_required=$true}|ConvertTo-Json -Compress
$editGrant=(Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key3} -ContentType 'application/json' -Body $editBody).Content|ConvertFrom-Json
$editGrantId=[string]$editGrant.data.id;$grantIds+=$editGrantId
$before=(Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/sharing-grants/$editGrantId/resource" -Headers @{Authorization="Bearer $editor"}).Content|ConvertFrom-Json
$editPayload=@{content='接收人通过共享编辑形成的新故事修订。';reason='修正合成故事表述'}|ConvertTo-Json -Compress
$edited=Invoke-WebRequest -UseBasicParsing -Method Patch -Uri "$api/api/v1/sharing-grants/$editGrantId/resource" -Headers @{Authorization="Bearer $editor";'If-Match'=('"v'+$before.data.version+'"')} -ContentType 'application/json' -Body $editPayload
$editedData=($edited.Content|ConvertFrom-Json).data
if($editedData.text-ne'接收人通过共享编辑形成的新故事修订。'-or$editedData.revision_no-ne($before.data.revision_no+1)){throw 'Shared story edit did not create a new revision'}
Expected {Invoke-WebRequest -UseBasicParsing -Method Patch -Uri "$api/api/v1/sharing-grants/$editGrantId/resource" -Headers @{Authorization="Bearer $editor";'If-Match'=('"v'+$before.data.version+'"')} -ContentType 'application/json' -Body $editPayload} 412
$anonymousBody=@{resource_type='STORY';resource_id=$storyPublicId;grantee_user_id=$editorPublicId;permission='VIEW_TEXT';purpose='匿名口令分享';expires_at=[DateTime]::UtcNow.AddMinutes(10).ToString('o');login_required=$false}|ConvertTo-Json -Compress
$anonymousResponse=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key4} -ContentType 'application/json' -Body $anonymousBody
$anonymous=($anonymousResponse.Content|ConvertFrom-Json).data;$anonymousId=[string]$anonymous.id;$shareToken=[string]$anonymous.access_token;$grantIds+=$anonymousId
if($shareToken.Length-lt32-or$anonymous.login_required-ne$false){throw 'Anonymous share token was not returned'}
$anonymousReplay=(Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/sharing-grants" -Headers @{Authorization="Bearer $owner";'Idempotency-Key'=$key4} -ContentType 'application/json' -Body $anonymousBody).Content|ConvertFrom-Json
if([string]$anonymousReplay.data.access_token-ne$shareToken){throw 'Anonymous idempotency replay did not derive the same token'}
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/public/shares/$anonymousId" -Headers @{'X-Share-Token'=('x'*43)}} 403
$publicShared=(Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/public/shares/$anonymousId" -Headers @{'X-Share-Token'=$shareToken}).Content|ConvertFrom-Json
if($publicShared.data.text-ne'接收人通过共享编辑形成的新故事修订。'){throw 'Anonymous token did not return the current story'}
$storedDigest=(docker compose exec -T postgres psql -U voice -d voice_platform -t -A -c "SELECT access_token_digest FROM sharing_grant WHERE public_id='$anonymousId'::uuid"|Out-String).Trim()
if($storedDigest.Length-ne64-or$storedDigest-eq$shareToken){throw 'Raw anonymous token was stored in PostgreSQL'}
Invoke-WebRequest -UseBasicParsing -Method Delete -Uri "$api/api/v1/sharing-grants/$anonymousId" -Headers @{Authorization="Bearer $owner"}|Out-Null
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/public/shares/$anonymousId" -Headers @{'X-Share-Token'=$shareToken}} 410
$counts=(docker compose exec -T postgres psql -U voice -d voice_platform -t -A -F '|' -c "SELECT count(*) FILTER(WHERE result='SUCCESS'),count(*) FILTER(WHERE result='DENIED') FROM sharing_access_log WHERE sharing_grant_id IN(SELECT id FROM sharing_grant WHERE public_id IN('$grantId'::uuid,'$expiredId'::uuid))"|Out-String).Trim()
if($counts-ne'1|3'){throw "Unexpected sharing audit counts: $counts"}
Write-Host "Resource sharing smoke passed: authenticated and anonymous access, deterministic token replay, story edit concurrency, isolation, revoke, expiry and access audit: $grantId"
}
finally{
$ids=($grantIds|Where-Object{$_-match'^[0-9a-fA-F-]{36}$'}|ForEach-Object{"'$_'::uuid"})-join','
if(!$ids){$ids="'00000000-0000-0000-0000-000000000000'::uuid"}
$cleanup=@"
BEGIN;
DELETE FROM idempotency_record WHERE scope='sharing-grant.create' AND (resource_id IN($ids) OR idempotency_key IN('$key1','$key2','$key3','$key4'));
DELETE FROM audit_event WHERE resource_id IN($ids) OR resource_id='$storyPublicId'::uuid;
DELETE FROM sharing_grant WHERE public_id IN($ids);
DELETE FROM web_event WHERE resource_public_id IN('$storyPublicId'::uuid,'$conversationPublicId'::uuid);
UPDATE story_draft SET current_revision_id=NULL WHERE public_id='$storyPublicId'::uuid;
DELETE FROM story_revision WHERE story_id=(SELECT id FROM story_draft WHERE public_id='$storyPublicId'::uuid);
DELETE FROM story_draft WHERE public_id='$storyPublicId'::uuid;
DELETE FROM conversation_turn WHERE conversation_id=(SELECT id FROM conversation WHERE public_id='$conversationPublicId'::uuid);
DELETE FROM conversation WHERE public_id='$conversationPublicId'::uuid;
DELETE FROM context_snapshot WHERE id='$contextId'::uuid;
COMMIT;
"@
$cleanup|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null
}