Migrate paypal and stripe interactions to durable background jobs.

This commit is contained in:
Alex Hart
2025-03-14 14:02:19 -03:00
committed by Cody Henthorne
parent ad00e7c5ab
commit 7cc4677120
54 changed files with 2221 additions and 1082 deletions

View File

@@ -4,26 +4,21 @@ import android.app.Application
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.reactivex.rxjava3.core.Flowable
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.testutil.RxPluginsRule
import java.util.concurrent.TimeUnit
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
@@ -80,7 +75,7 @@ class OneTimeInAppPaymentRepositoryTest {
}
@Test
fun `Given a registered non-self individual, when I verifyRecipientIsAllowedToReceiveAGift, then I expect completion`() {
fun `Given a registered non-self individual, when I verifyRecipientIsAllowedToReceiveAGift, then I expect no error`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
id = recipientId,
@@ -91,13 +86,10 @@ class OneTimeInAppPaymentRepositoryTest {
mockkStatic(Recipient::class)
every { Recipient.resolved(recipientId) } returns recipient
val testObserver = OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(recipientId).test()
rxRule.defaultScheduler.triggerActions()
testObserver.assertComplete()
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGiftSync(recipientId)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given self, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -108,7 +100,7 @@ class OneTimeInAppPaymentRepositoryTest {
verifyRecipientIsNotAllowedToBeGiftedBadges(recipient)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given an unregistered individual, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -120,7 +112,7 @@ class OneTimeInAppPaymentRepositoryTest {
verifyRecipientIsNotAllowedToBeGiftedBadges(recipient)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given a group, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -133,7 +125,7 @@ class OneTimeInAppPaymentRepositoryTest {
verifyRecipientIsNotAllowedToBeGiftedBadges(recipient)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given a call link, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -146,7 +138,7 @@ class OneTimeInAppPaymentRepositoryTest {
verifyRecipientIsNotAllowedToBeGiftedBadges(recipient)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given a distribution list, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -159,7 +151,7 @@ class OneTimeInAppPaymentRepositoryTest {
verifyRecipientIsNotAllowedToBeGiftedBadges(recipient)
}
@Test
@Test(expected = DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid::class)
fun `Given release notes, when I verifyRecipientIsAllowedToReceiveAGift, then I expect SelectedRecipientIsInvalid`() {
val recipientId = RecipientId.from(1L)
val recipient = Recipient(
@@ -210,68 +202,10 @@ class OneTimeInAppPaymentRepositoryTest {
.assertComplete()
}
@Test
fun `Given a long running transaction, when I waitForOneTimeRedemption, then I expect DonationPending`() {
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.SEPADebit)
every { SignalDatabase.Companion.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
val testObserver = OneTimeInAppPaymentRepository
.waitForOneTimeRedemption(inAppPayment, "test-intent-id")
.test()
rxRule.defaultScheduler.triggerActions()
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
rxRule.defaultScheduler.triggerActions()
testObserver
.assertError { it is DonationError.BadgeRedemptionError.DonationPending }
}
@Test
fun `Given a non long running transaction, when I waitForOneTimeRedemption, then I expect TimeoutWaitingForTokenError`() {
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.CreditCard)
every { SignalDatabase.Companion.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
val testObserver = OneTimeInAppPaymentRepository
.waitForOneTimeRedemption(inAppPayment, "test-intent-id")
.test()
rxRule.defaultScheduler.triggerActions()
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
rxRule.defaultScheduler.triggerActions()
testObserver
.assertError { it is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError }
}
@Test
fun `Given no delays, when I waitForOneTimeRedemption, then I expect happy path`() {
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.CreditCard)
every { InAppPaymentsRepository.observeUpdates(inAppPayment.id) } returns Flowable.just(inAppPayment.copy(state = InAppPaymentTable.State.END))
every { SignalDatabase.Companion.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
val testObserver = OneTimeInAppPaymentRepository
.waitForOneTimeRedemption(inAppPayment, "test-intent-id")
.test()
rxRule.defaultScheduler.triggerActions()
testObserver
.assertComplete()
}
private fun verifyRecipientIsNotAllowedToBeGiftedBadges(recipient: Recipient) {
mockkStatic(Recipient::class)
every { Recipient.resolved(recipient.id) } returns recipient
val testObserver = OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(recipient.id).test()
rxRule.defaultScheduler.triggerActions()
testObserver.assertError {
it is DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid
}
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGiftSync(recipient.id)
}
}

View File

@@ -9,7 +9,6 @@ import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import io.reactivex.rxjava3.core.Flowable
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -17,10 +16,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
@@ -28,17 +23,12 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.subscription.LevelUpdate
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.testutil.RxPluginsRule
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.EmptyResponse
import org.whispersystems.signalservice.internal.ServiceResponse
import java.util.Currency
import java.util.concurrent.TimeUnit
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
@@ -98,13 +88,10 @@ class RecurringInAppPaymentRepositoryTest {
val initialSubscriber = createSubscriber()
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess(initialSubscriber)
val testObserver = RecurringInAppPaymentRepository.ensureSubscriberId(
RecurringInAppPaymentRepository.ensureSubscriberIdSync(
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
isRotation = false
).test()
rxRule.defaultScheduler.triggerActions()
testObserver.assertComplete()
)
val newSubscriber = ref.get()
@@ -116,13 +103,10 @@ class RecurringInAppPaymentRepositoryTest {
val initialSubscriber = createSubscriber()
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess(initialSubscriber)
val testObserver = RecurringInAppPaymentRepository.ensureSubscriberId(
RecurringInAppPaymentRepository.ensureSubscriberIdSync(
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
isRotation = true
).test()
rxRule.defaultScheduler.triggerActions()
testObserver.assertComplete()
)
val newSubscriber = ref.get()
@@ -160,98 +144,6 @@ class RecurringInAppPaymentRepositoryTest {
}
}
@Test
fun `given no delays, when I setSubscriptionLevel, then I expect happy path`() {
val paymentSourceType = PaymentSourceType.Stripe.CreditCard
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
InAppPaymentsTestRule.mockLocalSubscriberAccess(createSubscriber())
every { SignalStore.inAppPayments.getLevelOperation("500") } returns LevelUpdateOperation(IdempotencyKey.generate(), "500")
every { SignalDatabase.inAppPayments.getById(any()) } returns inAppPayment
every { InAppPaymentsRepository.observeUpdates(inAppPayment.id) } returns Flowable.just(inAppPayment.copy(state = InAppPaymentTable.State.END))
val testObserver = RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType).test()
val processingUpdate = LevelUpdate.isProcessing.test()
rxRule.defaultScheduler.triggerActions()
processingUpdate.assertValues(false, true, false)
testObserver.assertComplete()
}
@Test
fun `given 10s delay, when I setSubscriptionLevel, then I expect timeout`() {
val paymentSourceType = PaymentSourceType.Stripe.CreditCard
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
InAppPaymentsTestRule.mockLocalSubscriberAccess(createSubscriber())
every { SignalStore.inAppPayments.getLevelOperation("500") } returns LevelUpdateOperation(IdempotencyKey.generate(), "500")
every { SignalDatabase.inAppPayments.getById(any()) } returns inAppPayment
val testObserver = RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType).test()
val processingUpdate = LevelUpdate.isProcessing.test()
rxRule.defaultScheduler.triggerActions()
processingUpdate.assertValues(false, true, false)
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
rxRule.defaultScheduler.triggerActions()
testObserver.assertError {
it is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError
}
}
@Test
fun `given long running payment type with 10s delay, when I setSubscriptionLevel, then I expect pending`() {
val paymentSourceType = PaymentSourceType.Stripe.SEPADebit
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
InAppPaymentsTestRule.mockLocalSubscriberAccess(createSubscriber())
every { SignalStore.inAppPayments.getLevelOperation("500") } returns LevelUpdateOperation(IdempotencyKey.generate(), "500")
every { SignalDatabase.inAppPayments.getById(any()) } returns inAppPayment
val testObserver = RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType).test()
val processingUpdate = LevelUpdate.isProcessing.test()
rxRule.defaultScheduler.triggerActions()
processingUpdate.assertValues(false, true, false)
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
rxRule.defaultScheduler.triggerActions()
testObserver.assertError {
it is DonationError.BadgeRedemptionError.DonationPending
}
}
@Test
fun `given an execution error, when I setSubscriptionLevel, then I expect the same error`() {
val expected = NonSuccessfulResponseCodeException(404)
val paymentSourceType = PaymentSourceType.Stripe.SEPADebit
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
InAppPaymentsTestRule.mockLocalSubscriberAccess(createSubscriber())
every { SignalStore.inAppPayments.getLevelOperation("500") } returns LevelUpdateOperation(IdempotencyKey.generate(), "500")
every { SignalDatabase.inAppPayments.getById(any()) } returns inAppPayment
every { AppDependencies.donationsService.updateSubscriptionLevel(any(), any(), any(), any(), any()) } returns ServiceResponse.forExecutionError(expected)
val testObserver = RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType).test()
val processingUpdate = LevelUpdate.isProcessing.distinctUntilChanged().test()
rxRule.defaultScheduler.triggerActions()
processingUpdate.assertValues(false, true, false)
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
rxRule.defaultScheduler.triggerActions()
testObserver.assertError(expected)
}
private fun createSubscriber(): InAppPaymentSubscriberRecord {
return InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),

View File

@@ -534,7 +534,7 @@ class InAppPaymentRecurringContextJobTest {
private fun insertInAppPayment(
type: InAppPaymentType = InAppPaymentType.RECURRING_DONATION,
state: InAppPaymentTable.State = InAppPaymentTable.State.CREATED,
state: InAppPaymentTable.State = InAppPaymentTable.State.TRANSACTING,
subscriberId: SubscriberId? = SubscriberId.generate(),
paymentSourceType: PaymentSourceType = PaymentSourceType.Stripe.CreditCard,
badge: BadgeList.Badge? = null,
@@ -551,6 +551,11 @@ class InAppPaymentRecurringContextJobTest {
endOfPeriod = null,
inAppPaymentData = iap.data.copy(
badge = badge,
waitForAuth = null,
stripeActionComplete = null,
payPalActionComplete = null,
payPalRequiresAction = null,
stripeRequiresAction = null,
redemption = redemptionState
)
)