Add unit tests for InAppPaymentRecurringContextJob.

This commit is contained in:
Alex Hart
2025-01-27 15:52:54 -04:00
committed by Greyson Parrelli
parent 762c7a6d22
commit b5f323d4af
6 changed files with 345 additions and 34 deletions

View File

@@ -17,24 +17,35 @@ import org.junit.rules.ExternalResource
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
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
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.util.JsonUtil
import java.math.BigDecimal
import java.util.Currency
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
* Common setup between different tests that rely on donations infrastructure.
*/
class DonationsTestRule : ExternalResource() {
class InAppPaymentsTestRule : ExternalResource() {
private var nextId = 1L
private val inAppPaymentCache = mutableMapOf<InAppPaymentTable.InAppPaymentId, InAppPaymentTable.InAppPayment>()
private val configuration: SubscriptionsConfiguration by lazy {
val testConfigJsonData = javaClass.classLoader!!.getResourceAsStream("donations_configuration_test_data.json").bufferedReader().readText()
@@ -58,14 +69,48 @@ class DonationsTestRule : ExternalResource() {
every { InAppDonations.isIDEALAvailable() } returns true
mockkObject(SignalDatabase.Companion)
every { SignalDatabase.Companion.donationReceipts } returns mockk {
every { SignalDatabase.Companion.donationReceipts.addReceipt(any()) } returns Unit
}
every { SignalDatabase.Companion.inAppPayments } returns mockk {
every { SignalDatabase.Companion.inAppPayments.update(any()) } returns Unit
every { SignalDatabase.Companion.inAppPayments.insert(any(), any(), any(), any(), any()) } answers {
val inAppPaymentData: InAppPaymentData = arg(4)
val iap = createInAppPayment(firstArg(), inAppPaymentData.paymentMethodType.toPaymentSourceType())
val id = InAppPaymentTable.InAppPaymentId(nextId)
nextId++
inAppPaymentCache[id] = iap.copy(
id = id,
state = secondArg(),
subscriberId = thirdArg(),
endOfPeriod = arg(3) ?: 0.seconds,
data = inAppPaymentData
)
id
}
every { SignalDatabase.Companion.inAppPayments.update(any()) } answers {
val inAppPayment = firstArg<InAppPaymentTable.InAppPayment>()
inAppPaymentCache[inAppPayment.id] = inAppPayment
}
every { SignalDatabase.Companion.inAppPayments.getById(any()) } answers {
val inAppPaymentId = firstArg<InAppPaymentTable.InAppPaymentId>()
inAppPaymentCache[inAppPaymentId]
}
}
mockkObject(SignalStore.Companion)
every { SignalStore.Companion.inAppPayments } returns mockk {
every { setLastEndOfPeriod(any()) } returns Unit
}
}
override fun after() {
unmockkStatic(RemoteConfig::class, InAppPaymentsRepository::class)
unmockkObject(InAppDonations, SignalDatabase.Companion)
unmockkObject(InAppDonations, SignalDatabase.Companion, SignalStore.Companion)
}
/**
@@ -75,6 +120,38 @@ class DonationsTestRule : ExternalResource() {
every { AppDependencies.donationsService.getDonationsConfiguration(any()) } returns ServiceResponse(200, "", configuration, null, null)
}
fun initializeActiveSubscriptionMock(
activeSubscription: ActiveSubscription? = null,
executionError: Throwable? = null,
applicationError: Throwable? = null
) {
every { AppDependencies.donationsService.getSubscription(any()) } returns ServiceResponse(200, "", activeSubscription, null, null)
}
fun initializeSubmitReceiptCredentialRequestSync() {
val receiptCredentialResponse = mockk<ReceiptCredentialResponse>()
every { AppDependencies.donationsService.submitReceiptCredentialRequestSync(any(), any()) } returns ServiceResponse(200, "", receiptCredentialResponse, null, null)
}
fun createActiveSubscription(): ActiveSubscription {
return ActiveSubscription(
ActiveSubscription.Subscription(
2000,
"USD",
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 45.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 45.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
}
fun createInAppPayment(
type: InAppPaymentType,
paymentSourceType: PaymentSourceType
@@ -96,4 +173,14 @@ class DonationsTestRule : ExternalResource() {
)
)
}
companion object {
fun mockLocalSubscriberAccess(initialSubscriber: InAppPaymentSubscriberRecord? = null): AtomicReference<InAppPaymentSubscriberRecord?> {
val ref = AtomicReference(initialSubscriber)
every { InAppPaymentsRepository.getSubscriber(any()) } answers { ref.get() }
every { InAppPaymentsRepository.setSubscriber(any()) } answers { ref.set(firstArg()) }
return ref
}
}
}

View File

@@ -36,7 +36,7 @@ class OneTimeInAppPaymentRepositoryTest {
val appDependencies = MockAppDependenciesRule()
@get:Rule
val donationsTestRule = DonationsTestRule()
val inAppPaymentsTestRule = InAppPaymentsTestRule()
@Test
fun `Given a throwable and self, when I handleCreatePaymentIntentError, then I expect a ONE_TIME error`() {
@@ -174,7 +174,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `When I getBoosts, then I expect a filtered set of boost objects`() {
donationsTestRule.initializeDonationsConfigurationMock()
inAppPaymentsTestRule.initializeDonationsConfigurationMock()
val testObserver = OneTimeInAppPaymentRepository.getBoosts().test()
rxRule.defaultScheduler.triggerActions()
@@ -188,7 +188,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `When I getBoostBadge, then I expect a boost badge`() {
donationsTestRule.initializeDonationsConfigurationMock()
inAppPaymentsTestRule.initializeDonationsConfigurationMock()
val testObserver = OneTimeInAppPaymentRepository.getBoostBadge().test()
rxRule.defaultScheduler.triggerActions()
@@ -200,7 +200,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `When I getMinimumDonationAmounts, then I expect a map of 3 currencies`() {
donationsTestRule.initializeDonationsConfigurationMock()
inAppPaymentsTestRule.initializeDonationsConfigurationMock()
val testObserver = OneTimeInAppPaymentRepository.getMinimumDonationAmounts().test()
rxRule.defaultScheduler.triggerActions()
@@ -212,7 +212,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `Given a long running transaction, when I waitForOneTimeRedemption, then I expect DonationPending`() {
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.SEPADebit)
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.SEPADebit)
every { SignalDatabase.Companion.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
@@ -230,7 +230,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `Given a non long running transaction, when I waitForOneTimeRedemption, then I expect TimeoutWaitingForTokenError`() {
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.CreditCard)
val inAppPayment = inAppPaymentsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.CreditCard)
every { SignalDatabase.Companion.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
@@ -248,7 +248,7 @@ class OneTimeInAppPaymentRepositoryTest {
@Test
fun `Given no delays, when I waitForOneTimeRedemption, then I expect happy path`() {
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.ONE_TIME_DONATION, PaymentSourceType.Stripe.CreditCard)
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

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.subscription
import android.app.Application
import androidx.lifecycle.AtomicReference
import assertk.assertThat
import assertk.assertions.isNotEqualTo
import io.mockk.every
@@ -52,10 +51,12 @@ class RecurringInAppPaymentRepositoryTest {
val appDependencies = MockAppDependenciesRule()
@get:Rule
val donationsTestRule = DonationsTestRule()
val inAppPaymentsTestRule = InAppPaymentsTestRule()
@Before
fun setUp() {
InAppPaymentsTestRule.mockLocalSubscriberAccess()
mockkObject(SignalStore.Companion)
every { SignalStore.Companion.inAppPayments } returns mockk {
every { SignalStore.Companion.inAppPayments.getRecurringDonationCurrency() } returns Currency.getInstance("USD")
@@ -81,7 +82,7 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `when I getDonationsConfiguration then I expect a set of three Subscription objects`() {
donationsTestRule.initializeDonationsConfigurationMock()
inAppPaymentsTestRule.initializeDonationsConfigurationMock()
val testObserver = RecurringInAppPaymentRepository.getSubscriptions().test()
rxRule.defaultScheduler.triggerActions()
@@ -95,7 +96,7 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `Given I do not need to rotate my subscriber id, when I ensureSubscriberId, then I use the same subscriber id`() {
val initialSubscriber = createSubscriber()
val ref = mockLocalSubscriberAccess(initialSubscriber)
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess(initialSubscriber)
val testObserver = RecurringInAppPaymentRepository.ensureSubscriberId(
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
@@ -113,7 +114,7 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `Given I need to rotate my subscriber id, when I ensureSubscriberId, then I generate and set a new subscriber id`() {
val initialSubscriber = createSubscriber()
val ref = mockLocalSubscriberAccess(initialSubscriber)
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess(initialSubscriber)
val testObserver = RecurringInAppPaymentRepository.ensureSubscriberId(
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
@@ -130,7 +131,7 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `Given no current subscriber, when I rotateSubscriberId, then I do not try to cancel subscription`() {
val ref = mockLocalSubscriberAccess()
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess()
val testObserver = RecurringInAppPaymentRepository.rotateSubscriberId(InAppPaymentSubscriberRecord.Type.DONATION).test()
@@ -146,7 +147,7 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `Given current subscriber, when I rotateSubscriberId, then I do not try to cancel subscription`() {
val initialSubscriber = createSubscriber()
val ref = mockLocalSubscriberAccess(initialSubscriber)
val ref = InAppPaymentsTestRule.mockLocalSubscriberAccess(initialSubscriber)
val testObserver = RecurringInAppPaymentRepository.rotateSubscriberId(InAppPaymentSubscriberRecord.Type.DONATION).test()
@@ -162,8 +163,8 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `given no delays, when I setSubscriptionLevel, then I expect happy path`() {
val paymentSourceType = PaymentSourceType.Stripe.CreditCard
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
mockLocalSubscriberAccess(createSubscriber())
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
@@ -182,8 +183,8 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `given 10s delay, when I setSubscriptionLevel, then I expect timeout`() {
val paymentSourceType = PaymentSourceType.Stripe.CreditCard
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
mockLocalSubscriberAccess(createSubscriber())
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
@@ -206,8 +207,8 @@ class RecurringInAppPaymentRepositoryTest {
@Test
fun `given long running payment type with 10s delay, when I setSubscriptionLevel, then I expect pending`() {
val paymentSourceType = PaymentSourceType.Stripe.SEPADebit
val inAppPayment = donationsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
mockLocalSubscriberAccess(createSubscriber())
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
@@ -231,8 +232,8 @@ class RecurringInAppPaymentRepositoryTest {
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 = donationsTestRule.createInAppPayment(InAppPaymentType.RECURRING_DONATION, paymentSourceType)
mockLocalSubscriberAccess(createSubscriber())
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
@@ -261,12 +262,4 @@ class RecurringInAppPaymentRepositoryTest {
iapSubscriptionId = null
)
}
private fun mockLocalSubscriberAccess(initialSubscriber: InAppPaymentSubscriberRecord? = null): AtomicReference<InAppPaymentSubscriberRecord?> {
val ref = AtomicReference(initialSubscriber)
every { InAppPaymentsRepository.getSubscriber(any()) } answers { ref.get() }
every { InAppPaymentsRepository.setSubscriber(any()) } answers { ref.set(firstArg()) }
return ref
}
}