import { googlePlayRestoreAvailabilityError, googlePlayAccountHash, isEntitlementForRestoredProducts, isVerifiedActivePurchase, restoreGooglePlayPurchaseSet, type IapVerifyResult, } from '../src/lib/billing-context' import type { EntitlementSnapshot } from '../src/lib/entitlement-context' import type { Purchase } from 'react-native-iap' import { normalizeMobileRuntimeConfig } from '../src/lib/native-config' const PURCHASE_ID = '11111111-1111-4111-8111-111111111111' function googlePurchase( productId = 'd3ro_voice_pro_monthly', token = `play_${'a'.repeat(32)}`, overrides: Partial = {}, ): Purchase { return { id: `order-${token}`, productId, purchaseToken: token, purchaseState: 'purchased', store: 'google', transactionDate: Date.parse('2026-08-21T00:00:00.000Z'), quantity: 1, isAutoRenewing: true, packageNameAndroid: 'com.d3ro.voice', ...overrides, } as Purchase } function verification( productId = 'd3ro_voice_pro_monthly', overrides: { state?: string entitled?: boolean acknowledged?: boolean serverAcknowledged?: boolean } = {}, ): IapVerifyResult { const acknowledged = overrides.acknowledged ?? true return { purchase: { purchase_id: PURCHASE_ID, acknowledged }, verification: { product_id: productId, purchase_state: overrides.state ?? 'purchased', entitled: overrides.entitled ?? true, acknowledged, }, finish_transaction: false, server_acknowledged: overrides.serverAcknowledged ?? acknowledged, } } function entitlement( productId: 'd3ro_voice_pro_monthly' | 'd3ro_voice_pro_plus_monthly' = 'd3ro_voice_pro_monthly', overrides: Partial = {}, ): EntitlementSnapshot { const tier = productId === 'd3ro_voice_pro_plus_monthly' ? 'pro_plus' : 'pro' return { tier, status: 'active', provider: 'google_play', paymentProvider: 'google_play', currentPeriodStart: '2026-08-21T00:00:00.000Z', currentPeriodEnd: '2026-09-21T00:00:00.000Z', cancelAt: null, autoRenewing: true, storeProductId: productId, overageCredits: 0, usageToday: {}, purchases: [{ id: PURCHASE_ID, platform: 'google_play', productId, state: 'purchased', purchaseAt: '2026-08-21T00:00:00.000Z', expiresAt: '2026-09-21T00:00:00.000Z', autoRenewing: true, verifiedAt: '2026-08-21T00:00:01.000Z', }], adFree: true, refreshedAt: '2026-08-21T00:00:02.000Z', ...overrides, } } describe('mobile monetization identity and native configuration', () => { test('matches the server Google Play account binding hash', () => { expect(googlePlayAccountHash('00000000-0000-4000-8000-000000000001')).toBe( 'a648437059eb4bc9c85c416a34e40419f7dd1e45c985aa701a23f5d86cd76678', ) }) test('accepts canonical debug ad units', () => { expect(normalizeMobileRuntimeConfig({ debug: true, packageName: 'com.d3ro.voice', adMobBannerUnitId: 'ca-app-pub-3940256099942544/6300978111', adMobRewardedUnitId: 'ca-app-pub-3940256099942544/5224354917', playStoreInstalled: true, installedFromPlayStore: false, installationId: '00000000-0000-4000-8000-000000000001', deviceName: 'Pixel Test', osVersion: 'Android 14', appVersion: '1.0.0', })).toEqual(expect.objectContaining({ debug: true, packageName: 'com.d3ro.voice' })) }) test('rejects test ad units in production', () => { expect(() => normalizeMobileRuntimeConfig({ debug: false, packageName: 'com.d3ro.voice', adMobBannerUnitId: 'ca-app-pub-3940256099942544/6300978111', adMobRewardedUnitId: 'ca-app-pub-1234567890123456/1111111111', playStoreInstalled: true, installedFromPlayStore: true, installationId: '00000000-0000-4000-8000-000000000001', deviceName: 'Pixel Test', osVersion: 'Android 14', appVersion: '1.0.0', })).toThrow('production_ad_unit_uses_test_id') }) test('rejects a mismatched Android package', () => { expect(() => normalizeMobileRuntimeConfig({ debug: true, packageName: 'com.example.wrong', adMobBannerUnitId: 'ca-app-pub-3940256099942544/6300978111', adMobRewardedUnitId: 'ca-app-pub-3940256099942544/5224354917', playStoreInstalled: true, installedFromPlayStore: true, installationId: '00000000-0000-4000-8000-000000000001', deviceName: 'Pixel Test', osVersion: 'Android 14', appVersion: '1.0.0', })).toThrow('mobile_native_config_invalid') }) test('accepts restoration only after the server verifies an active acknowledged purchase', () => { expect(isVerifiedActivePurchase({ verification: { product_id: 'd3ro_voice_pro_monthly', purchase_state: 'purchased', entitled: true, acknowledged: true, }, server_acknowledged: true, }, 'd3ro_voice_pro_monthly')).toBe(true) }) test('accepts a cancelled but still entitled and acknowledged subscription', () => { expect(isVerifiedActivePurchase({ verification: { product_id: 'd3ro_voice_pro_monthly', purchase_state: 'cancelled', entitled: true, acknowledged: true, }, server_acknowledged: true, }, 'd3ro_voice_pro_monthly')).toBe(true) }) test.each([ ['missing verification', { server_acknowledged: true }], ['pending', { verification: { product_id: 'd3ro_voice_pro_monthly', purchase_state: 'pending', entitled: false, acknowledged: false, }, server_acknowledged: false, }], ['wrong product', { verification: { product_id: 'd3ro_voice_pro_plus_monthly', purchase_state: 'purchased', entitled: true, acknowledged: true, }, server_acknowledged: true, }], ['unacknowledged', { verification: { product_id: 'd3ro_voice_pro_monthly', purchase_state: 'purchased', entitled: true, acknowledged: false, }, server_acknowledged: false, }], ])('rejects restoration for %s', (_label, result) => { expect(isVerifiedActivePurchase(result, 'd3ro_voice_pro_monthly')).toBe(false) }) }) describe('Google Play restore boundary', () => { test.each([ [{ installedFromPlayStore: false, connected: true, platform: 'android', userId: 'user' }, 'play_store_install_required'], [{ installedFromPlayStore: true, connected: false, platform: 'android', userId: 'user' }, 'store_connection_failed'], [{ installedFromPlayStore: true, connected: true, platform: 'ios', userId: 'user' }, 'restore_not_supported'], [{ installedFromPlayStore: true, connected: true, platform: 'android', userId: null }, 'restore_not_supported'], ])('fails explicitly when the store boundary is unavailable', (input, expected) => { expect(googlePlayRestoreAvailabilityError(input)).toBe(expected) }) test('does not report a restore when the store has no supported purchases', async () => { const verifyPurchase = jest.fn() const refreshEntitlement = jest.fn() await expect(restoreGooglePlayPurchaseSet([], verifyPurchase, refreshEntitlement)) .rejects.toThrow('no_restorable_purchases') await expect(restoreGooglePlayPurchaseSet([ googlePurchase('legacy_unknown_product'), ], verifyPurchase, refreshEntitlement)).rejects.toThrow('no_restorable_purchases') expect(verifyPurchase).not.toHaveBeenCalled() expect(refreshEntitlement).not.toHaveBeenCalled() }) test('deduplicates store rows and succeeds only after the refreshed matching tier is visible', async () => { const purchase = googlePurchase() const verifyPurchase = jest.fn(async () => verification()) const refreshed = entitlement() const refreshEntitlement = jest.fn(async () => refreshed) await expect(restoreGooglePlayPurchaseSet( [purchase, { ...purchase }], verifyPurchase, refreshEntitlement, )).resolves.toBe(refreshed) expect(verifyPurchase).toHaveBeenCalledTimes(1) expect(verifyPurchase).toHaveBeenCalledWith(purchase) expect(refreshEntitlement).toHaveBeenCalledTimes(1) expect(isEntitlementForRestoredProducts( refreshed, new Set(['d3ro_voice_pro_monthly']), )).toBe(true) }) test('fails the whole restore when one supported purchase cannot prove ownership', async () => { const purchases = [ googlePurchase('d3ro_voice_pro_monthly', `play_${'a'.repeat(32)}`), googlePurchase('d3ro_voice_pro_plus_monthly', `play_${'b'.repeat(32)}`), ] const verifyPurchase = jest.fn(async (purchase: Purchase) => { if (purchase.productId === 'd3ro_voice_pro_plus_monthly') { throw new Error('purchase_account_mismatch') } return verification(purchase.productId) }) const refreshEntitlement = jest.fn() await expect(restoreGooglePlayPurchaseSet( purchases, verifyPurchase, refreshEntitlement, )).rejects.toThrow('purchase_account_mismatch') expect(verifyPurchase).toHaveBeenCalledTimes(2) expect(refreshEntitlement).not.toHaveBeenCalled() }) test('verifies pending purchases but never promotes them to restored', async () => { const verifyPurchase = jest.fn(async () => verification( 'd3ro_voice_pro_monthly', { state: 'pending', entitled: false, acknowledged: false, serverAcknowledged: false }, )) const refreshEntitlement = jest.fn() await expect(restoreGooglePlayPurchaseSet( [googlePurchase('d3ro_voice_pro_monthly', undefined, { purchaseState: 'pending' })], verifyPurchase, refreshEntitlement, )).rejects.toThrow('no_active_subscription_to_restore') expect(verifyPurchase).toHaveBeenCalledTimes(1) expect(refreshEntitlement).not.toHaveBeenCalled() }) test('rejects an entitled purchase unless server acknowledgement and persistence agree', async () => { const refreshEntitlement = jest.fn() await expect(restoreGooglePlayPurchaseSet( [googlePurchase()], async () => verification('d3ro_voice_pro_monthly', { acknowledged: false, serverAcknowledged: false, }), refreshEntitlement, )).rejects.toThrow('restore_acknowledgement_missing') expect(refreshEntitlement).not.toHaveBeenCalled() }) test('keeps transaction finishing server-owned instead of accepting a client finish request', async () => { const result = verification() result.finish_transaction = true await expect(restoreGooglePlayPurchaseSet( [googlePurchase()], async () => result, jest.fn(), )).rejects.toThrow('restore_verification_response_invalid') }) test('rejects success when entitlement refresh does not expose the verified product tier', async () => { await expect(restoreGooglePlayPurchaseSet( [googlePurchase()], async () => verification(), async () => entitlement('d3ro_voice_pro_plus_monthly'), )).rejects.toThrow('restored_entitlement_mismatch') }) test.each([ [googlePurchase('d3ro_voice_pro_monthly', 'short'), 'restore_purchase_payload_invalid'], [googlePurchase('d3ro_voice_pro_monthly', undefined, { store: 'amazon' }), 'restore_purchase_source_invalid'], [googlePurchase('d3ro_voice_pro_monthly', undefined, { packageNameAndroid: 'com.attacker.app' }), 'restore_purchase_package_mismatch'], ])('rejects malformed or foreign supported store rows', async (purchase, expected) => { await expect(restoreGooglePlayPurchaseSet( [purchase], jest.fn(), jest.fn(), )).rejects.toThrow(expected) }) })