feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
177
server/supabase/functions/_shared/google-play.test.ts
Normal file
177
server/supabase/functions/_shared/google-play.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import {
|
||||
acknowledgeGooglePlaySubscription,
|
||||
googlePlayAccountId,
|
||||
GooglePlayVerificationError,
|
||||
normalizeGooglePlaySubscription,
|
||||
verifyGooglePlaySubscriptionPayload,
|
||||
} from './google-play.ts'
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
const futureExpiry = '2030-01-01T00:00:00Z'
|
||||
const pastExpiry = '2020-01-01T00:00:00Z'
|
||||
|
||||
function standardBase64(bytes: Uint8Array): string {
|
||||
let binary = ''
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte)
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
Deno.test('active Google Play subscription grants the mapped tier', () => {
|
||||
const normalized = normalizeGooglePlaySubscription({
|
||||
startTime: '2029-12-01T00:00:00Z',
|
||||
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
|
||||
acknowledgementState: 'ACKNOWLEDGEMENT_STATE_PENDING',
|
||||
linkedPurchaseToken: 'old-linked-purchase-token',
|
||||
lineItems: [{
|
||||
productId: 'd3ro_voice_pro_monthly',
|
||||
expiryTime: futureExpiry,
|
||||
latestSuccessfulOrderId: 'GPA.1234-5678-9012-34567',
|
||||
autoRenewingPlan: { autoRenewEnabled: true },
|
||||
}],
|
||||
}, 'd3ro_voice_pro_monthly', Date.parse('2029-12-15T00:00:00Z'))
|
||||
|
||||
assert(normalized.tier === 'pro', 'product must map to pro')
|
||||
assert(normalized.entitled, 'active future subscription must be entitled')
|
||||
assert(normalized.purchaseState === 'purchased', 'active subscription must be purchased')
|
||||
assert(normalized.autoRenewing, 'auto renewal must be preserved')
|
||||
assert(!normalized.acknowledged, 'pending acknowledgement must be preserved')
|
||||
assert(normalized.linkedPurchaseToken === 'old-linked-purchase-token', 'linked token must be preserved')
|
||||
assert(normalized.storeTransactionId === 'GPA.1234-5678-9012-34567', 'line-item order id must be preserved')
|
||||
})
|
||||
|
||||
Deno.test('out-of-app re-subscription requires a verified prior account binding', async () => {
|
||||
const userId = '11111111-2222-3333-4444-555555555555'
|
||||
const accountId = await googlePlayAccountId(userId)
|
||||
const base = {
|
||||
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
|
||||
lineItems: [{ productId: 'd3ro_voice_pro_monthly', expiryTime: futureExpiry }],
|
||||
}
|
||||
|
||||
const byExpiredAccount = await verifyGooglePlaySubscriptionPayload(
|
||||
userId,
|
||||
'd3ro_voice_pro_monthly',
|
||||
{
|
||||
...base,
|
||||
outOfAppPurchaseContext: {
|
||||
expiredExternalAccountIdentifiers: { obfuscatedExternalAccountId: accountId },
|
||||
},
|
||||
},
|
||||
)
|
||||
assert(byExpiredAccount.entitled, 'expired account id should bind the re-subscription')
|
||||
|
||||
const byExpiredToken = await verifyGooglePlaySubscriptionPayload(
|
||||
userId,
|
||||
'd3ro_voice_pro_monthly',
|
||||
{
|
||||
...base,
|
||||
outOfAppPurchaseContext: { expiredPurchaseToken: 'known-expired-token' },
|
||||
},
|
||||
(token) => Promise.resolve(token === 'known-expired-token'),
|
||||
)
|
||||
assert(byExpiredToken.entitled, 'server-owned expired token should bind the re-subscription')
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
await verifyGooglePlaySubscriptionPayload(
|
||||
userId,
|
||||
'd3ro_voice_pro_monthly',
|
||||
{ ...base, outOfAppPurchaseContext: { expiredPurchaseToken: 'unknown-token' } },
|
||||
() => Promise.resolve(false),
|
||||
)
|
||||
} catch (error) {
|
||||
rejected = error instanceof GooglePlayVerificationError
|
||||
&& error.code === 'purchase_account_mismatch'
|
||||
}
|
||||
assert(rejected, 'unowned out-of-app purchase must fail closed')
|
||||
})
|
||||
|
||||
Deno.test('canceled subscription keeps access only until its verified expiry', () => {
|
||||
const beforeExpiry = normalizeGooglePlaySubscription({
|
||||
subscriptionState: 'SUBSCRIPTION_STATE_CANCELED',
|
||||
lineItems: [{ productId: 'd3ro_voice_pro_plus_monthly', expiryTime: futureExpiry }],
|
||||
}, 'd3ro_voice_pro_plus_monthly', Date.parse('2029-12-15T00:00:00Z'))
|
||||
assert(beforeExpiry.purchaseState === 'cancelled', 'state must be cancelled')
|
||||
assert(beforeExpiry.entitled, 'canceled subscription retains prepaid access')
|
||||
|
||||
const afterExpiry = normalizeGooglePlaySubscription({
|
||||
subscriptionState: 'SUBSCRIPTION_STATE_CANCELED',
|
||||
lineItems: [{ productId: 'd3ro_voice_pro_plus_monthly', expiryTime: pastExpiry }],
|
||||
}, 'd3ro_voice_pro_plus_monthly', Date.parse('2029-12-15T00:00:00Z'))
|
||||
assert(!afterExpiry.entitled, 'expired canceled subscription must not grant access')
|
||||
})
|
||||
|
||||
Deno.test('unknown and mismatched products fail closed', () => {
|
||||
for (const productId of ['unknown_product', 'd3ro_voice_pro_monthly']) {
|
||||
let error: unknown
|
||||
try {
|
||||
normalizeGooglePlaySubscription({
|
||||
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
|
||||
lineItems: [{ productId: 'different_product', expiryTime: futureExpiry }],
|
||||
}, productId)
|
||||
} catch (candidate) {
|
||||
error = candidate
|
||||
}
|
||||
assert(error instanceof GooglePlayVerificationError, 'invalid product must fail explicitly')
|
||||
}
|
||||
})
|
||||
|
||||
Deno.test('obfuscated account id is deterministic and contains no raw UUID', async () => {
|
||||
const userId = '11111111-2222-3333-4444-555555555555'
|
||||
const accountId = await googlePlayAccountId(userId)
|
||||
assert(accountId.length === 64, 'account hash must be SHA-256 hex')
|
||||
assert(!accountId.includes(userId), 'raw user id must not be exposed to the store')
|
||||
assert(accountId === await googlePlayAccountId(userId), 'account hash must be stable')
|
||||
})
|
||||
|
||||
Deno.test('Google Play subscription is acknowledged by the server API', async () => {
|
||||
const keyPair = await crypto.subtle.generateKey({
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
modulusLength: 2048,
|
||||
publicExponent: Uint8Array.from([1, 0, 1]),
|
||||
hash: 'SHA-256',
|
||||
}, true, ['sign', 'verify'])
|
||||
const privateKey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', keyPair.privateKey))
|
||||
const privateKeyBase64 = standardBase64(privateKey).match(/.{1,64}/g)?.join('\n') ?? ''
|
||||
const serviceAccount = {
|
||||
client_email: 'play-verifier@example.iam.gserviceaccount.com',
|
||||
private_key: `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----\n`,
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
}
|
||||
Deno.env.set('GOOGLE_PLAY_SERVICE_ACCOUNT_JSON', JSON.stringify(serviceAccount))
|
||||
Deno.env.set('GOOGLE_PLAY_PACKAGE_NAME', 'com.d3ro.voice')
|
||||
const requestedUrls: string[] = []
|
||||
const fakeFetch: typeof fetch = (input, init) => {
|
||||
const url = String(input)
|
||||
requestedUrls.push(url)
|
||||
if (url === 'https://oauth2.googleapis.com/token') {
|
||||
return Promise.resolve(new Response(JSON.stringify({ access_token: 'fixture-access-token', expires_in: 3600 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
}
|
||||
const requestInit = init as { method?: string; headers?: unknown } | undefined
|
||||
assert(requestInit?.method === 'POST', 'acknowledgement must use POST')
|
||||
assert(requestInit.headers !== undefined, 'acknowledgement must be authenticated')
|
||||
return Promise.resolve(new Response(null, { status: 204 }))
|
||||
}
|
||||
|
||||
try {
|
||||
await acknowledgeGooglePlaySubscription(
|
||||
'd3ro_voice_pro_monthly',
|
||||
'fixture-purchase-token',
|
||||
fakeFetch,
|
||||
)
|
||||
} finally {
|
||||
Deno.env.delete('GOOGLE_PLAY_SERVICE_ACCOUNT_JSON')
|
||||
Deno.env.delete('GOOGLE_PLAY_PACKAGE_NAME')
|
||||
}
|
||||
|
||||
assert(requestedUrls.length === 2, 'OAuth and acknowledge endpoints must both be called')
|
||||
assert(
|
||||
requestedUrls[1].endsWith('/purchases/subscriptions/d3ro_voice_pro_monthly/tokens/fixture-purchase-token:acknowledge'),
|
||||
'acknowledge endpoint must bind package, product and purchase token',
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue