diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt index e46aab1e9d..3f3f5f62a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt @@ -50,7 +50,7 @@ class InAppPaymentRecurringContextJob private constructor( const val KEY = "InAppPurchaseRecurringContextJob" - fun create(inAppPayment: InAppPaymentTable.InAppPayment): Job { + fun create(inAppPayment: InAppPaymentTable.InAppPayment): InAppPaymentRecurringContextJob { return InAppPaymentRecurringContextJob( inAppPaymentId = inAppPayment.id, parameters = Parameters.Builder() diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationsTestRule.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsTestRule.kt similarity index 51% rename from app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationsTestRule.kt rename to app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsTestRule.kt index 45acff567d..002ea8a7d9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationsTestRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsTestRule.kt @@ -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() 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() + inAppPaymentCache[inAppPayment.id] = inAppPayment + } + + every { SignalDatabase.Companion.inAppPayments.getById(any()) } answers { + val inAppPaymentId = firstArg() + 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() + 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 { + val ref = AtomicReference(initialSubscriber) + every { InAppPaymentsRepository.getSubscriber(any()) } answers { ref.get() } + every { InAppPaymentsRepository.setSubscriber(any()) } answers { ref.set(firstArg()) } + + return ref + } + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeInAppPaymentRepositoryTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeInAppPaymentRepositoryTest.kt index 340240d4b5..06fe6ed4bc 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeInAppPaymentRepositoryTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeInAppPaymentRepositoryTest.kt @@ -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 diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepositoryTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepositoryTest.kt index 8b12ee7cb3..31c0f089b4 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepositoryTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepositoryTest.kt @@ -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 { - val ref = AtomicReference(initialSubscriber) - every { InAppPaymentsRepository.getSubscriber(any()) } answers { ref.get() } - every { InAppPaymentsRepository.setSubscriber(any()) } answers { ref.set(firstArg()) } - - return ref - } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJobTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJobTest.kt new file mode 100644 index 0000000000..a259549231 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJobTest.kt @@ -0,0 +1,230 @@ +package org.thoughtcrime.securesms.jobs + +import android.app.Application +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isTrue +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import org.junit.Before +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.core.util.logging.Log +import org.signal.donations.InAppPaymentType +import org.signal.donations.PaymentSourceType +import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsTestRule +import org.thoughtcrime.securesms.database.InAppPaymentTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.SystemOutLogger +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription +import org.whispersystems.signalservice.api.subscriptions.SubscriberId +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class InAppPaymentRecurringContextJobTest { + + @get:Rule + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val inAppPaymentsTestRule = InAppPaymentsTestRule() + + @Before + fun setUp() { + Log.initialize(SystemOutLogger()) + + mockkObject(InAppPaymentsRepository) + every { InAppPaymentsRepository.generateRequestCredential() } returns mockk { + every { serialize() } returns byteArrayOf() + every { request } returns mockk() + } + + inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync() + + val minimumTime = System.currentTimeMillis().milliseconds + 60.days + val minimumTimeS = minimumTime.inWholeSeconds + val offset = minimumTimeS % 86400L + val actualMinimumTime = minimumTimeS - offset + + every { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) } returns mockk() { + every { receiptLevel } returns 2000 + every { receiptExpirationTime } returns actualMinimumTime + } + } + + @Test + fun `Given a CREATED IAP, when I onAdded, then I expect PENDING`() { + val iap = insertInAppPayment() + val job = InAppPaymentRecurringContextJob.create(iap) + + job.onAdded() + val updatedIap = SignalDatabase.inAppPayments.getById(iap.id) + + assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.PENDING) + } + + @Test + fun `Given an IAP without an error, when I onFailure, then I set error and move to end state`() { + val iap = insertInAppPayment() + val job = InAppPaymentRecurringContextJob.create(iap) + + job.onFailure() + val updatedIap = SignalDatabase.inAppPayments.getById(iap.id) + + assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END) + assertThat(updatedIap?.data?.error).isNotNull() + } + + @Test + fun `Given a SEPA IAP, when I getNextRunAttemptBackoff, then I expect one day`() { + val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.SEPADebit) + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.getNextRunAttemptBackoff(1, Exception()) + + assertThat(result).isEqualTo(1.days.inWholeMilliseconds) + } + + @Test + fun `Given an iDEAL IAP, when I getNextRunAttemptBackoff, then I expect one day`() { + val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL) + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.getNextRunAttemptBackoff(1, Exception()) + + assertThat(result).isEqualTo(1.days.inWholeMilliseconds) + } + + @Test + fun `Test happy path for subscription redemption`() { + val activeSubscription = inAppPaymentsTestRule.createActiveSubscription() + inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription) + + val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL) + val job = InAppPaymentRecurringContextJob.create(iap) + job.onAdded() + + val result = job.run() + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `Given END state, when I run, then I expect failure`() { + val iap = insertInAppPayment( + state = InAppPaymentTable.State.END + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given non-recurring IAP, when I run, then I expect failure`() { + val iap = insertInAppPayment( + type = InAppPaymentType.ONE_TIME_GIFT + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given no subscriber id, when I run, then I expect failure`() { + val iap = insertInAppPayment( + subscriberId = null + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given no redemption data, when I run, then I expect failure`() { + val iap = insertInAppPayment( + redemptionState = null + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given redemption started, when I run, then I expect failure`() { + val iap = insertInAppPayment( + redemptionState = InAppPaymentData.RedemptionState( + stage = InAppPaymentData.RedemptionState.Stage.REDEMPTION_STARTED + ) + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given redeemed, when I run, then I expect failure`() { + val iap = insertInAppPayment( + redemptionState = InAppPaymentData.RedemptionState( + stage = InAppPaymentData.RedemptionState.Stage.REDEEMED + ) + ) + + val job = InAppPaymentRecurringContextJob.create(iap) + + val result = job.run() + assertThat(result.isFailure).isTrue() + } + + @Test + fun `Given no available subscription, when I run, then I expect retry`() { + val iap = insertInAppPayment() + val job = InAppPaymentRecurringContextJob.create(iap) + inAppPaymentsTestRule.initializeActiveSubscriptionMock(ActiveSubscription(null, null)) + + val result = job.run() + assertThat(result.isRetry).isTrue() + } + + private fun insertInAppPayment( + type: InAppPaymentType = InAppPaymentType.RECURRING_DONATION, + state: InAppPaymentTable.State = InAppPaymentTable.State.CREATED, + subscriberId: SubscriberId? = SubscriberId.generate(), + paymentSourceType: PaymentSourceType = PaymentSourceType.Stripe.CreditCard, + redemptionState: InAppPaymentData.RedemptionState? = InAppPaymentData.RedemptionState( + stage = InAppPaymentData.RedemptionState.Stage.INIT + ) + ): InAppPaymentTable.InAppPayment { + val iap = inAppPaymentsTestRule.createInAppPayment(type, paymentSourceType) + SignalDatabase.inAppPayments.insert( + type = iap.type, + state = state, + subscriberId = subscriberId, + endOfPeriod = null, + inAppPaymentData = iap.data.copy( + redemption = redemptionState + ) + ) + + return iap + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java index 691f092299..b46c6e08cc 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java @@ -37,6 +37,7 @@ public final class SystemOutLogger extends Log.Logger { private String format(char level, String tag, String message, Throwable t) { if (t != null) { + t.printStackTrace(); return String.format("%c[%s] %s %s:%s", level, tag, message, t.getClass().getSimpleName(), t.getMessage()); } else { return String.format("%c[%s] %s", level, tag, message);