Implement initial support for IAP data.

This commit is contained in:
Alex Hart
2024-12-19 16:34:29 -04:00
committed by Greyson Parrelli
parent f537fa6436
commit f2b4bd0585
35 changed files with 957 additions and 241 deletions

View File

@@ -108,11 +108,12 @@ class CheckoutFlowActivityTest__RecurringDonations {
currency = currency,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = null
)
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
@@ -149,11 +150,12 @@ class CheckoutFlowActivityTest__RecurringDonations {
currency = currency,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = null
)
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {

View File

@@ -0,0 +1,201 @@
package org.thoughtcrime.securesms.database
import android.database.sqlite.SQLiteConstraintException
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.count
import org.signal.core.util.deleteAll
import org.signal.core.util.readToSingleInt
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
class InAppPaymentSubscriberTableTest {
@get:Rule
val harness = SignalActivityRule()
@Before
fun setUp() {
SignalDatabase.inAppPaymentSubscribers.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
}
@Test(expected = SQLiteConstraintException::class)
fun givenASubscriberWithCurrencyAndIAPData_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance("USD"),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
fail("Expected a thrown exception.")
}
@Test(expected = SQLiteConstraintException::class)
fun givenADonorSubscriberWithGoogleIAPData_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
fail("Expected a thrown exception.")
}
@Test(expected = SQLiteConstraintException::class)
fun givenADonorSubscriberWithAppleIAPData_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
fail("Expected a thrown exception.")
}
@Test(expected = SQLiteConstraintException::class)
fun givenADonorSubscriberWithoutCurrency_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = null
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
fail("Expected a thrown exception.")
}
@Test
fun givenADonorSubscriberWithCurrency_whenITryToInsert_thenIExpectSuccess() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance("USD"),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
iapSubscriptionId = null
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
}
@Test(expected = SQLiteConstraintException::class)
fun givenABackupSubscriberWithCurrency_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance("USD"),
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = null
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
fail("Expected a thrown exception.")
}
@Test(expected = SQLiteConstraintException::class)
fun givenABackupSubscriberWithoutIAPData_whenITryToInsert_thenIExpectException() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = null
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
}
@Test
fun givenABackupSubscriberWithGoogleIAPData_whenITryToInsert_thenIExpectSuccess() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
}
@Test
fun givenABackupSubscriberWithAppleIAPData_whenITryToInsert_thenIExpectSuccess() {
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
}
@Test
fun givenABackupSubscriberWithAppleIAPData_whenITryToInsertAGoogleSubscriber_thenIExpectSuccess() {
val appleSubscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(appleSubscriber)
val googleSubscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(googleSubscriber)
val subscriberCount = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.run()
.readToSingleInt()
subscriberCount assertIs 1
val subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
subscriber.iapSubscriptionId?.originalTransactionId assertIs null
subscriber.iapSubscriptionId?.purchaseToken assertIs "testToken"
subscriber.subscriberId assertIs googleSubscriber.subscriberId
}
}

View File

@@ -104,7 +104,8 @@ class FixInAppCurrencyIfAbleTest {
currency = Currency.getInstance(currencyCode),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL,
iapSubscriptionId = null
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)

View File

@@ -14,6 +14,7 @@ import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import okio.ByteString
import org.signal.core.util.Base64
import org.signal.core.util.billing.BillingApi
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
@@ -49,6 +50,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val recipientCache: LiveRecipientCache
private var signalServiceMessageSender: SignalServiceMessageSender? = null
private var billingApi: BillingApi = mockk()
init {
runSync {
@@ -108,6 +110,8 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
recipientCache = LiveRecipientCache(application) { r -> r.run() }
}
override fun provideBillingApi(): BillingApi = billingApi
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
return serviceNetworkAccessMock
}

View File

@@ -0,0 +1,162 @@
package org.thoughtcrime.securesms.migrations
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.verify
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.billing.BillingPurchaseState
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
@RunWith(AndroidJUnit4::class)
class GooglePlayBillingPurchaseTokenMigrationJobTest {
@get:Rule
val harness = SignalActivityRule()
@Before
fun setUp() {
SignalDatabase.inAppPaymentSubscribers.writableDatabase.deleteAll(InAppPaymentSubscriberTable.TABLE_NAME)
}
@Test
fun givenNoSubscribers_whenIRunJob_thenIExpectNoBillingAccess() {
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
verify { AppDependencies.billingApi wasNot Called }
}
@Test
fun givenSubscriberWithAppleData_whenIRunJob_thenIExpectNoBillingAccess() {
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
)
)
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
verify { AppDependencies.billingApi wasNot Called }
}
@Test
fun givenSubscriberWithGoogleToken_whenIRunJob_thenIExpectNoBillingAccess() {
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
)
)
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
verify { AppDependencies.billingApi wasNot Called }
}
@Test
fun givenSubscriberWithPlaceholderAndNoBillingAccess_whenIRunJob_thenIExpectNoUpdate() {
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
sub?.iapSubscriptionId?.purchaseToken assertIs "-"
}
@Test
fun givenSubscriberWithPlaceholderAndNoPurchase_whenIRunJob_thenIExpectNoUpdate() {
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
sub?.iapSubscriptionId?.purchaseToken assertIs "-"
}
@Test
fun givenSubscriberWithPurchase_whenIRunJob_thenIExpectUpdate() {
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = null,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
purchaseState = BillingPurchaseState.PURCHASED,
purchaseToken = "purchaseToken",
isAcknowledged = true,
purchaseTime = System.currentTimeMillis(),
isAutoRenewing = true
)
val job = GooglePlayBillingPurchaseTokenMigrationJob()
job.run()
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
sub?.iapSubscriptionId?.purchaseToken assertIs "purchaseToken"
}
}

View File

@@ -36,14 +36,14 @@ class SubscriberIdMigrationJobTest {
@Test
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
val subscriberId = SubscriberId.generate()
SignalStore.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
SignalStore.inAppPayments.setRecurringDonationCurrency(Currency.getInstance("USD"))
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD")
actual.assertIsNotNull()
actual!!.subscriberId.bytes assertIs subscriberId.bytes