$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 }