652 lines
27 KiB
PowerShell
652 lines
27 KiB
PowerShell
param(
|
|
[string]$SupabaseExe = 'C:\Users\encep\bin\supabase.exe',
|
|
[string]$ProjectUrl = 'http://127.0.0.1:55321'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$supabaseProjectDirectory = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
$createdUserIds = [System.Collections.Generic.List[string]]::new()
|
|
$createdAdRewardTransactionIds = [System.Collections.Generic.List[string]]::new()
|
|
$passCount = 0
|
|
|
|
function Assert-Equal {
|
|
param(
|
|
[string]$Name,
|
|
[object]$Actual,
|
|
[object]$Expected
|
|
)
|
|
|
|
if ($Actual -ne $Expected) {
|
|
throw "FAIL $Name expected=[$Expected] actual=[$Actual]"
|
|
}
|
|
$script:passCount += 1
|
|
Write-Host "PASS $Name"
|
|
}
|
|
|
|
function Assert-True {
|
|
param(
|
|
[string]$Name,
|
|
[bool]$Condition
|
|
)
|
|
|
|
if (-not $Condition) {
|
|
throw "FAIL $Name"
|
|
}
|
|
$script:passCount += 1
|
|
Write-Host "PASS $Name"
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $SupabaseExe)) {
|
|
throw "Supabase CLI was not found at $SupabaseExe"
|
|
}
|
|
|
|
$statusEnv = & $SupabaseExe status --workdir $supabaseProjectDirectory -o env 2>$null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'D3RO local Supabase stack is not running.'
|
|
}
|
|
|
|
function Get-StatusValue {
|
|
param([string]$Name)
|
|
|
|
$line = $statusEnv | Where-Object { $_ -like ($Name + '=*') }
|
|
if (-not $line) {
|
|
throw "Supabase status did not provide $Name"
|
|
}
|
|
return (($line -split '=', 2)[1]).Trim('"')
|
|
}
|
|
|
|
$anonKey = Get-StatusValue 'ANON_KEY'
|
|
$serviceRoleKey = Get-StatusValue 'SERVICE_ROLE_KEY'
|
|
$testPassword = 'D3ro-Mobile-Platform-E2E-2026!'
|
|
|
|
function Invoke-JsonRequest {
|
|
param(
|
|
[ValidateSet('Get', 'Post', 'Patch', 'Delete')]
|
|
[string]$Method,
|
|
[string]$Path,
|
|
[string]$ApiKey,
|
|
[string]$Bearer,
|
|
[object]$Body,
|
|
[hashtable]$AdditionalHeaders = @{}
|
|
)
|
|
|
|
$headers = @{
|
|
apikey = $ApiKey
|
|
Authorization = "Bearer $Bearer"
|
|
}
|
|
foreach ($header in $AdditionalHeaders.GetEnumerator()) {
|
|
$headers[$header.Key] = $header.Value
|
|
}
|
|
|
|
$parameters = @{
|
|
Uri = $ProjectUrl + $Path
|
|
Method = $Method
|
|
Headers = $headers
|
|
TimeoutSec = 30
|
|
SkipHttpErrorCheck = $true
|
|
}
|
|
if ($null -ne $Body) {
|
|
$parameters.ContentType = 'application/json'
|
|
$parameters.Body = $Body | ConvertTo-Json -Depth 8
|
|
}
|
|
|
|
return Invoke-WebRequest @parameters
|
|
}
|
|
|
|
function Convert-ResponseJson {
|
|
param([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$Response)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Response.Content)) {
|
|
return $null
|
|
}
|
|
return $Response.Content | ConvertFrom-Json
|
|
}
|
|
|
|
function Get-Sha256Hex {
|
|
param([string]$Value)
|
|
|
|
$algorithm = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
|
|
return [Convert]::ToHexString($algorithm.ComputeHash($bytes)).ToLowerInvariant()
|
|
} finally {
|
|
$algorithm.Dispose()
|
|
}
|
|
}
|
|
|
|
function New-TestUser {
|
|
param([string]$Label)
|
|
|
|
$email = "mobile-platform-$Label-$([guid]::NewGuid().ToString('N'))@example.test"
|
|
$response = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/auth/v1/signup' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $anonKey `
|
|
-Body @{ email = $email; password = $testPassword; data = @{ name = "Mobile $Label" } }
|
|
|
|
Assert-Equal "signup-$Label-status" ([int]$response.StatusCode) 200
|
|
$body = Convert-ResponseJson $response
|
|
Assert-True "signup-$Label-session" (-not [string]::IsNullOrWhiteSpace($body.access_token))
|
|
$createdUserIds.Add([string]$body.user.id)
|
|
return $body
|
|
}
|
|
|
|
function Remove-TestUser {
|
|
param([string]$UserId)
|
|
|
|
$response = Invoke-JsonRequest `
|
|
-Method Delete `
|
|
-Path "/auth/v1/admin/users/$UserId" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
if ([int]$response.StatusCode -notin @(200, 404)) {
|
|
Write-Warning "Fixture cleanup failed for $UserId with status $($response.StatusCode)"
|
|
}
|
|
}
|
|
|
|
function Remove-TestAdRewardReceiptByTransaction {
|
|
param([string]$TransactionId)
|
|
|
|
$response = Invoke-JsonRequest `
|
|
-Method Delete `
|
|
-Path "/rest/v1/ad_reward_receipts?transaction_id=eq.$TransactionId" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
if ([int]$response.StatusCode -notin @(200, 204)) {
|
|
Write-Warning "Ad reward receipt fixture cleanup failed for transaction $TransactionId with status $($response.StatusCode)"
|
|
}
|
|
}
|
|
|
|
try {
|
|
$userOne = New-TestUser 'owner'
|
|
$userTwo = New-TestUser 'other'
|
|
|
|
$unsignedAdCallback = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/functions/v1/admob-ssv?reward_amount=50' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $anonKey `
|
|
-Body $null
|
|
# Invalid signed/unsigned callbacks are acknowledged with 200 so AdMob does
|
|
# not retry fraudulent input; no reward RPC is reached on this path.
|
|
Assert-Equal 'admob-unsigned-callback-acknowledged-without-grant' ([int]$unsignedAdCallback.StatusCode) 200
|
|
Assert-True 'admob-unsigned-callback-error' $unsignedAdCallback.Content.Contains('missing_signature')
|
|
|
|
$iapRequestBody = @{
|
|
platform = 'google_play'
|
|
productId = 'd3ro_voice_pro_monthly'
|
|
purchaseToken = 'unverified-fixture-purchase-token'
|
|
}
|
|
$iapUnauthenticated = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/iap-verify' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $anonKey `
|
|
-Body $iapRequestBody
|
|
Assert-Equal 'iap-edge-unauthenticated-denied' ([int]$iapUnauthenticated.StatusCode) 401
|
|
|
|
$unknownIapBody = $iapRequestBody.Clone()
|
|
$unknownIapBody.productId = 'unknown_product'
|
|
$iapUnknownProduct = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/iap-verify' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $unknownIapBody
|
|
Assert-Equal 'iap-edge-unknown-product-denied' ([int]$iapUnknownProduct.StatusCode) 400
|
|
|
|
$iapUnconfigured = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/iap-verify' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $iapRequestBody
|
|
Assert-Equal 'iap-edge-unconfigured-fails-closed' ([int]$iapUnconfigured.StatusCode) 503
|
|
Assert-True 'iap-edge-unconfigured-error' $iapUnconfigured.Content.Contains('google_play_not_configured')
|
|
|
|
$rtdnUnconfigured = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/google-play-rtdn' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $anonKey `
|
|
-Body @{ message = @{ messageId = 'fixture'; data = 'e30=' } }
|
|
Assert-Equal 'rtdn-edge-unconfigured-fails-closed' ([int]$rtdnUnconfigured.StatusCode) 503
|
|
Assert-True 'rtdn-edge-unconfigured-error' $rtdnUnconfigured.Content.Contains('google_pubsub_not_configured')
|
|
|
|
$settingsInsert = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/user_settings' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body @{
|
|
user_id = $userOne.user.id
|
|
theme_mode = 'dark'
|
|
locale = 'ko'
|
|
onboarding_version = 1
|
|
} `
|
|
-AdditionalHeaders @{ Prefer = 'return=minimal' }
|
|
Assert-Equal 'settings-owner-insert' ([int]$settingsInsert.StatusCode) 201
|
|
|
|
$crossSettings = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/user_settings?select=user_id&user_id=eq.$($userOne.user.id)" `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userTwo.access_token `
|
|
-Body $null
|
|
Assert-Equal 'settings-cross-user-read-status' ([int]$crossSettings.StatusCode) 200
|
|
Assert-Equal 'settings-cross-user-read-empty' @((Convert-ResponseJson $crossSettings)).Count 0
|
|
|
|
$forgedSettings = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/user_settings' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userTwo.access_token `
|
|
-Body @{ user_id = $userOne.user.id; theme_mode = 'light'; locale = 'en' }
|
|
Assert-Equal 'settings-cross-user-insert-denied' ([int]$forgedSettings.StatusCode) 403
|
|
|
|
$dictionaryInsert = Invoke-JsonRequest -Method Post -Path '/rest/v1/dictionary?select=id,word,category' -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ user_id = $userOne.user.id; word = ' D3RO Voice '; pronunciation = '디쓰리로 보이스'; category = 'technical' } -AdditionalHeaders @{ Prefer = 'return=representation' }
|
|
Assert-Equal 'dictionary-normalized-insert-status' ([int]$dictionaryInsert.StatusCode) 201
|
|
$dictionaryRow = @((Convert-ResponseJson $dictionaryInsert))[0]
|
|
Assert-Equal 'dictionary-normalized-word' $dictionaryRow.word 'D3RO Voice'
|
|
$dictionaryDuplicate = Invoke-JsonRequest -Method Post -Path '/rest/v1/dictionary' -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ user_id = $userOne.user.id; word = 'd3ro voice'; category = 'technical' }
|
|
Assert-Equal 'dictionary-case-duplicate-denied' ([int]$dictionaryDuplicate.StatusCode) 409
|
|
|
|
$bootstrapCommands = Invoke-JsonRequest -Method Post -Path '/rest/v1/rpc/bootstrap_custom_instructions' -ApiKey $anonKey -Bearer $userOne.access_token -Body @{}
|
|
Assert-Equal 'commands-bootstrap-owner-status' ([int]$bootstrapCommands.StatusCode) 200
|
|
$ownerBuiltins = @((Convert-ResponseJson $bootstrapCommands))
|
|
Assert-Equal 'commands-bootstrap-owner-count' $ownerBuiltins.Count 4
|
|
$bootstrapOtherCommands = Invoke-JsonRequest -Method Post -Path '/rest/v1/rpc/bootstrap_custom_instructions' -ApiKey $anonKey -Bearer $userTwo.access_token -Body @{}
|
|
Assert-Equal 'commands-bootstrap-other-status' ([int]$bootstrapOtherCommands.StatusCode) 200
|
|
Assert-Equal 'commands-bootstrap-other-count' @((Convert-ResponseJson $bootstrapOtherCommands)).Count 4
|
|
$crossCommands = Invoke-JsonRequest -Method Get -Path "/rest/v1/custom_instructions?select=id&user_id=eq.$($userOne.user.id)" -ApiKey $anonKey -Bearer $userTwo.access_token -Body $null
|
|
Assert-Equal 'commands-cross-user-read-empty' @((Convert-ResponseJson $crossCommands)).Count 0
|
|
|
|
$customCommandInsert = Invoke-JsonRequest -Method Post -Path '/rest/v1/custom_instructions?select=id,name,revision' -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ user_id = $userOne.user.id; name = 'Release summary'; description = 'Summarize a release'; prompt = 'Summarize this release without inventing facts.'; sort_order = 100 } -AdditionalHeaders @{ Prefer = 'return=representation' }
|
|
Assert-Equal 'commands-custom-insert-status' ([int]$customCommandInsert.StatusCode) 201
|
|
$customCommand = @((Convert-ResponseJson $customCommandInsert))[0]
|
|
$customCommandUpdate = Invoke-JsonRequest -Method Patch -Path "/rest/v1/custom_instructions?id=eq.$($customCommand.id)&revision=eq.1&select=id,revision" -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ name = 'Release notes summary' } -AdditionalHeaders @{ Prefer = 'return=representation' }
|
|
Assert-Equal 'commands-custom-update-revision' @((Convert-ResponseJson $customCommandUpdate))[0].revision 2
|
|
$staleCommandUpdate = Invoke-JsonRequest -Method Patch -Path "/rest/v1/custom_instructions?id=eq.$($customCommand.id)&revision=eq.1&select=id" -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ description = 'stale write' } -AdditionalHeaders @{ Prefer = 'return=representation' }
|
|
Assert-Equal 'commands-custom-stale-empty' @((Convert-ResponseJson $staleCommandUpdate)).Count 0
|
|
$activateCommand = Invoke-JsonRequest -Method Post -Path '/rest/v1/rpc/set_active_custom_instruction' -ApiKey $anonKey -Bearer $userOne.access_token -Body @{ instruction_id = $customCommand.id }
|
|
Assert-Equal 'commands-activate-id' (Convert-ResponseJson $activateCommand).active_instruction_id $customCommand.id
|
|
$crossActivateCommand = Invoke-JsonRequest -Method Post -Path '/rest/v1/rpc/set_active_custom_instruction' -ApiKey $anonKey -Bearer $userTwo.access_token -Body @{ instruction_id = $customCommand.id }
|
|
Assert-True 'commands-cross-user-activate-denied' ([int]$crossActivateCommand.StatusCode -ge 400)
|
|
|
|
$forgedJob = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/processing_jobs' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body @{
|
|
user_id = $userOne.user.id
|
|
kind = 'transcription'
|
|
idempotency_key = 'forged-client-job'
|
|
}
|
|
Assert-Equal 'processing-job-client-write-denied' ([int]$forgedJob.StatusCode) 403
|
|
|
|
$transactionId = 'txn-' + [guid]::NewGuid().ToString('N')
|
|
$createdAdRewardTransactionIds.Add($transactionId)
|
|
$rewardBody = @{
|
|
p_user_id = $userOne.user.id
|
|
p_network = 'admob'
|
|
p_placement = 'rewarded_video_quota'
|
|
p_ad_unit_id = 'google-demo-rewarded'
|
|
p_transaction_id = $transactionId
|
|
p_reward_tokens = 50
|
|
}
|
|
|
|
$unknownRewardBody = $rewardBody.Clone()
|
|
$unknownRewardBody.p_user_id = [guid]::NewGuid().ToString()
|
|
$unknownRewardBody.p_transaction_id = 'txn-' + [guid]::NewGuid().ToString('N')
|
|
$createdAdRewardTransactionIds.Add([string]$unknownRewardBody.p_transaction_id)
|
|
$unknownReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $unknownRewardBody
|
|
Assert-Equal 'reward-unknown-user-status' ([int]$unknownReward.StatusCode) 200
|
|
$unknownRewardResponse = Convert-ResponseJson $unknownReward
|
|
Assert-Equal 'reward-unknown-user-not-granted' $unknownRewardResponse.granted $false
|
|
Assert-Equal 'reward-unknown-user-reason' $unknownRewardResponse.reason 'unknown_user'
|
|
|
|
$unknownRewardReplay = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $unknownRewardBody
|
|
$unknownRewardReplayResponse = Convert-ResponseJson $unknownRewardReplay
|
|
Assert-Equal 'reward-unknown-user-replay-not-granted' $unknownRewardReplayResponse.granted $false
|
|
Assert-Equal 'reward-unknown-user-replay-duplicate' $unknownRewardReplayResponse.reason 'duplicate'
|
|
|
|
$unknownRewardReceipt = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/ad_reward_receipts?select=user_id,disposition&transaction_id=eq.$($unknownRewardBody.p_transaction_id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
$unknownRewardReceiptRows = @((Convert-ResponseJson $unknownRewardReceipt))
|
|
Assert-Equal 'reward-unknown-user-receipt-count' $unknownRewardReceiptRows.Count 1
|
|
Assert-Equal 'reward-unknown-user-receipt-unlinked' $unknownRewardReceiptRows[0].user_id $null
|
|
Assert-Equal 'reward-unknown-user-receipt-disposition' $unknownRewardReceiptRows[0].disposition 'unknown_user'
|
|
|
|
$clientReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $rewardBody
|
|
Assert-Equal 'reward-client-rpc-denied' ([int]$clientReward.StatusCode) 403
|
|
|
|
$serviceReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $rewardBody
|
|
Assert-Equal 'reward-service-status' ([int]$serviceReward.StatusCode) 200
|
|
$serviceRewardBody = Convert-ResponseJson $serviceReward
|
|
Assert-Equal 'reward-service-granted' $serviceRewardBody.granted $true
|
|
Assert-Equal 'reward-service-tokens' $serviceRewardBody.tokens_added 50
|
|
|
|
$duplicateReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $rewardBody
|
|
$duplicateBody = Convert-ResponseJson $duplicateReward
|
|
Assert-Equal 'reward-duplicate-not-granted' $duplicateBody.granted $false
|
|
Assert-Equal 'reward-duplicate-reason' $duplicateBody.reason 'duplicate'
|
|
|
|
$cooldownBody = $rewardBody.Clone()
|
|
$cooldownBody.p_transaction_id = 'txn-' + [guid]::NewGuid().ToString('N')
|
|
$createdAdRewardTransactionIds.Add([string]$cooldownBody.p_transaction_id)
|
|
$cooldownReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $cooldownBody
|
|
$cooldownResponse = Convert-ResponseJson $cooldownReward
|
|
Assert-Equal 'reward-cooldown-not-granted' $cooldownResponse.granted $false
|
|
Assert-Equal 'reward-cooldown-reason' $cooldownResponse.reason 'cooldown'
|
|
|
|
$ownerClaims = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/rest/v1/ad_reward_claims?select=transaction_id,reward_tokens' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
Assert-Equal 'reward-owner-claim-count' @((Convert-ResponseJson $ownerClaims)).Count 1
|
|
|
|
$otherClaims = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/rest/v1/ad_reward_claims?select=transaction_id,reward_tokens' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userTwo.access_token `
|
|
-Body $null
|
|
Assert-Equal 'reward-cross-user-claim-count' @((Convert-ResponseJson $otherClaims)).Count 0
|
|
|
|
$purchaseToken = 'fixture-google-play-token-' + [guid]::NewGuid().ToString('N')
|
|
$purchaseBody = @{
|
|
p_user_id = $userOne.user.id
|
|
p_platform = 'google_play'
|
|
p_product_id = 'd3ro_voice_pro_monthly'
|
|
p_store_transaction_id = 'GPA.fixture.' + [guid]::NewGuid().ToString('N')
|
|
p_token_hash = Get-Sha256Hex $purchaseToken
|
|
p_linked_token_hash = $null
|
|
p_purchase_token = $purchaseToken
|
|
p_purchase_state = 'purchased'
|
|
p_purchase_at = '2026-08-21T00:00:00Z'
|
|
p_expires_at = '2030-08-21T00:00:00Z'
|
|
p_auto_renewing = $true
|
|
p_acknowledged = $false
|
|
p_tier = 'pro'
|
|
p_entitled = $true
|
|
p_verification = @{ subscriptionState = 'SUBSCRIPTION_STATE_ACTIVE' }
|
|
}
|
|
|
|
$clientPurchase = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/apply_verified_google_play_purchase' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $purchaseBody
|
|
Assert-Equal 'iap-client-rpc-denied' ([int]$clientPurchase.StatusCode) 403
|
|
|
|
$servicePurchase = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/apply_verified_google_play_purchase' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $purchaseBody
|
|
Assert-Equal 'iap-service-rpc-status' ([int]$servicePurchase.StatusCode) 200
|
|
$servicePurchaseBody = Convert-ResponseJson $servicePurchase
|
|
Assert-Equal 'iap-service-entitled' $servicePurchaseBody.entitled $true
|
|
Assert-Equal 'iap-service-tier' $servicePurchaseBody.tier 'pro'
|
|
|
|
$ownerPurchases = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/rest/v1/iap_purchases?select=product_id,purchase_state' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
Assert-Equal 'iap-owner-summary-count' @((Convert-ResponseJson $ownerPurchases)).Count 1
|
|
|
|
$otherPurchases = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/rest/v1/iap_purchases?select=product_id,purchase_state' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userTwo.access_token `
|
|
-Body $null
|
|
Assert-Equal 'iap-cross-user-summary-count' @((Convert-ResponseJson $otherPurchases)).Count 0
|
|
|
|
$subscriptionProbe = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/subscriptions?select=tier,provider,payment_provider&user_id=eq.$($userOne.user.id)" `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
$subscriptionRows = @((Convert-ResponseJson $subscriptionProbe))
|
|
Assert-Equal 'iap-subscription-tier' $subscriptionRows[0].tier 'pro'
|
|
Assert-Equal 'iap-subscription-provider' $subscriptionRows[0].provider 'google_play'
|
|
|
|
$paidRewardBody = $rewardBody.Clone()
|
|
$paidRewardBody.p_transaction_id = 'txn-' + [guid]::NewGuid().ToString('N')
|
|
$createdAdRewardTransactionIds.Add([string]$paidRewardBody.p_transaction_id)
|
|
$paidReward = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $paidRewardBody
|
|
$paidRewardResponse = Convert-ResponseJson $paidReward
|
|
Assert-Equal 'reward-paid-tier-not-granted' $paidRewardResponse.granted $false
|
|
Assert-Equal 'reward-paid-tier-reason' $paidRewardResponse.reason 'ineligible_tier'
|
|
|
|
$stolenPurchaseBody = $purchaseBody.Clone()
|
|
$stolenPurchaseBody.p_user_id = $userTwo.user.id
|
|
$stolenPurchase = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/apply_verified_google_play_purchase' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $stolenPurchaseBody
|
|
Assert-Equal 'iap-cross-account-token-denied' ([int]$stolenPurchase.StatusCode) 400
|
|
Assert-True 'iap-cross-account-token-error' $stolenPurchase.Content.Contains('purchase_owned_by_other_user')
|
|
|
|
$replacementToken = 'fixture-google-play-replacement-' + [guid]::NewGuid().ToString('N')
|
|
$replacementBody = $purchaseBody.Clone()
|
|
$replacementBody.p_product_id = 'd3ro_voice_pro_plus_monthly'
|
|
$replacementBody.p_store_transaction_id = 'GPA.fixture.replacement.' + [guid]::NewGuid().ToString('N')
|
|
$replacementBody.p_token_hash = Get-Sha256Hex $replacementToken
|
|
$replacementBody.p_linked_token_hash = $purchaseBody.p_token_hash
|
|
$replacementBody.p_purchase_token = $replacementToken
|
|
$replacementBody.p_tier = 'pro_plus'
|
|
$replacementPurchase = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/apply_verified_google_play_purchase' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $replacementBody
|
|
Assert-Equal 'iap-linked-replacement-status' ([int]$replacementPurchase.StatusCode) 200
|
|
$replacementResponse = Convert-ResponseJson $replacementPurchase
|
|
Assert-Equal 'iap-linked-old-token-revoked' $replacementResponse.linked_purchase_revoked $true
|
|
Assert-Equal 'iap-linked-new-tier' $replacementResponse.tier 'pro_plus'
|
|
|
|
$linkedRowsProbe = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/iap_purchases?select=token_hash,purchase_state&user_id=eq.$($userOne.user.id)&order=created_at.asc" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
$linkedRows = @((Convert-ResponseJson $linkedRowsProbe))
|
|
Assert-Equal 'iap-linked-purchase-count' $linkedRows.Count 2
|
|
Assert-Equal 'iap-linked-old-state' $linkedRows[0].purchase_state 'expired'
|
|
|
|
$expiredPurchaseBody = $replacementBody.Clone()
|
|
$expiredPurchaseBody.p_linked_token_hash = $null
|
|
$expiredPurchaseBody.p_purchase_state = 'expired'
|
|
$expiredPurchaseBody.p_purchase_at = '2026-07-20T00:00:00Z'
|
|
$expiredPurchaseBody.p_expires_at = '2026-08-20T00:00:00Z'
|
|
$expiredPurchaseBody.p_auto_renewing = $false
|
|
$expiredPurchaseBody.p_acknowledged = $true
|
|
$expiredPurchaseBody.p_entitled = $false
|
|
$expiredPurchase = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/apply_verified_google_play_purchase' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $expiredPurchaseBody
|
|
Assert-Equal 'iap-expiry-rpc-status' ([int]$expiredPurchase.StatusCode) 200
|
|
|
|
$expiredSubscription = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/subscriptions?select=tier,provider,overage_credits&user_id=eq.$($userOne.user.id)" `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
$expiredSubscriptionRows = @((Convert-ResponseJson $expiredSubscription))
|
|
Assert-Equal 'iap-expiry-tier-downgrade' $expiredSubscriptionRows[0].tier 'free'
|
|
Assert-Equal 'iap-expiry-provider-clear' $expiredSubscriptionRows[0].provider 'none'
|
|
|
|
# The paid-tier callback above was valid and verified, but ineligible. Once
|
|
# the account becomes free again, replaying that exact transaction must stay
|
|
# unpaid forever instead of becoming newly grantable in another isolate.
|
|
$ineligibleReplay = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/rest/v1/rpc/grant_verified_ad_reward' `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $paidRewardBody
|
|
Assert-Equal 'reward-ineligible-replay-status' ([int]$ineligibleReplay.StatusCode) 200
|
|
$ineligibleReplayResponse = Convert-ResponseJson $ineligibleReplay
|
|
Assert-Equal 'reward-ineligible-replay-not-granted' $ineligibleReplayResponse.granted $false
|
|
Assert-Equal 'reward-ineligible-replay-duplicate' $ineligibleReplayResponse.reason 'duplicate'
|
|
|
|
$ineligibleReplayClaims = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/ad_reward_claims?select=id&transaction_id=eq.$($paidRewardBody.p_transaction_id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
Assert-Equal 'reward-ineligible-replay-claim-count' @((Convert-ResponseJson $ineligibleReplayClaims)).Count 0
|
|
|
|
$ineligibleReceipt = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/ad_reward_receipts?select=disposition&transaction_id=eq.$($paidRewardBody.p_transaction_id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
$ineligibleReceiptRows = @((Convert-ResponseJson $ineligibleReceipt))
|
|
Assert-Equal 'reward-ineligible-receipt-count' $ineligibleReceiptRows.Count 1
|
|
Assert-Equal 'reward-ineligible-receipt-disposition' $ineligibleReceiptRows[0].disposition 'ineligible_tier'
|
|
|
|
$postReplaySubscription = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/subscriptions?select=overage_credits&user_id=eq.$($userOne.user.id)" `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
$postReplaySubscriptionRows = @((Convert-ResponseJson $postReplaySubscription))
|
|
Assert-Equal 'reward-ineligible-replay-balance-unchanged' `
|
|
$postReplaySubscriptionRows[0].overage_credits `
|
|
$expiredSubscriptionRows[0].overage_credits
|
|
|
|
$receiptProbe = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/rest/v1/iap_purchase_receipts?select=purchase_id' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
Assert-Equal 'iap-receipt-owner-hidden-status' ([int]$receiptProbe.StatusCode) 200
|
|
Assert-Equal 'iap-receipt-owner-hidden-rows' @((Convert-ResponseJson $receiptProbe)).Count 0
|
|
|
|
$wrongDelete = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/account-delete' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body @{ confirmation = 'WRONG' }
|
|
Assert-Equal 'account-delete-confirmation-denied' ([int]$wrongDelete.StatusCode) 400
|
|
|
|
$accountDelete = Invoke-JsonRequest `
|
|
-Method Post `
|
|
-Path '/functions/v1/account-delete' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body @{ confirmation = 'DELETE_MY_ACCOUNT' }
|
|
Assert-Equal 'account-delete-success' ([int]$accountDelete.StatusCode) 200
|
|
|
|
$deletedToken = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path '/auth/v1/user' `
|
|
-ApiKey $anonKey `
|
|
-Bearer $userOne.access_token `
|
|
-Body $null
|
|
Assert-Equal 'account-delete-token-invalidated' ([int]$deletedToken.StatusCode) 403
|
|
|
|
$deletedProfile = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/profiles?select=id&id=eq.$($userOne.user.id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
Assert-Equal 'account-delete-profile-cascade' @((Convert-ResponseJson $deletedProfile)).Count 0
|
|
|
|
$deletedSubscription = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/subscriptions?select=id&user_id=eq.$($userOne.user.id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
Assert-Equal 'account-delete-subscription-cascade' @((Convert-ResponseJson $deletedSubscription)).Count 0
|
|
|
|
$deletedAccountRewardReceipt = Invoke-JsonRequest `
|
|
-Method Get `
|
|
-Path "/rest/v1/ad_reward_receipts?select=user_id,disposition&transaction_id=eq.$($paidRewardBody.p_transaction_id)" `
|
|
-ApiKey $serviceRoleKey `
|
|
-Bearer $serviceRoleKey `
|
|
-Body $null
|
|
$deletedAccountRewardReceiptRows = @((Convert-ResponseJson $deletedAccountRewardReceipt))
|
|
Assert-Equal 'account-delete-reward-receipt-retained' $deletedAccountRewardReceiptRows.Count 1
|
|
Assert-Equal 'account-delete-reward-receipt-unlinked' $deletedAccountRewardReceiptRows[0].user_id $null
|
|
Assert-Equal 'account-delete-reward-receipt-disposition' `
|
|
$deletedAccountRewardReceiptRows[0].disposition `
|
|
'ineligible_tier'
|
|
|
|
Write-Host "PASS mobile platform integration: $passCount assertions"
|
|
} finally {
|
|
foreach ($transactionId in $createdAdRewardTransactionIds) {
|
|
Remove-TestAdRewardReceiptByTransaction $transactionId
|
|
}
|
|
foreach ($userId in $createdUserIds) {
|
|
Remove-TestUser $userId
|
|
}
|
|
}
|