mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add unit testing for SharedInAppPaymentPipeline.
This commit is contained in:
committed by
Cody Henthorne
parent
ca5754cff3
commit
f6aa324d41
@@ -13,6 +13,7 @@ import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
@@ -93,7 +94,7 @@ object SharedInAppPaymentPipeline {
|
||||
Completable.error(InAppPaymentError(iap.data.error))
|
||||
} else {
|
||||
Log.d(TAG, "Unexpected early end state. Possible payment failure.", true)
|
||||
Completable.error(DonationError.genericPaymentFailure(DonationErrorSource.MONTHLY))
|
||||
Completable.error(DonationError.genericPaymentFailure(inAppPayment.type.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,18 +123,15 @@ object SharedInAppPaymentPipeline {
|
||||
BadgeRedemptionError.TimeoutWaitingForTokenError(errorSource)
|
||||
}
|
||||
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Awaiting completion of redemption chain for up to 10 seconds.", true)
|
||||
InAppPaymentsRepository.observeUpdates(inAppPayment.id).filter {
|
||||
it.state == InAppPaymentTable.State.END
|
||||
}.take(1).map {
|
||||
if (it.data.error != null) {
|
||||
Log.d(TAG, "Failure during redemption chain: ${it.data.error}", true)
|
||||
throw InAppPaymentError(it.data.error)
|
||||
}
|
||||
it
|
||||
}.firstOrError()
|
||||
}.timeout(10, TimeUnit.SECONDS, Single.error(timeoutError)).ignoreElement()
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPayment.id).filter {
|
||||
it.state == InAppPaymentTable.State.END
|
||||
}.take(1).map {
|
||||
if (it.data.error != null) {
|
||||
Log.d(TAG, "Failure during redemption chain: ${it.data.error}", true)
|
||||
throw InAppPaymentError(it.data.error)
|
||||
}
|
||||
it
|
||||
}.firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError)).ignoreElement()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import org.json.JSONObject
|
||||
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.donations.CreditCardPaymentSource
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PayPalPaymentSource
|
||||
import org.signal.donations.SEPADebitPaymentSource
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsTestRule
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPayPalOneTimeSetupJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPayPalRecurringSetupJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentStripeOneTimeSetupJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentStripeRecurringSetupJob
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
import org.thoughtcrime.securesms.testutil.RxPluginsRule
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class SharedInAppPaymentPipelineTest {
|
||||
|
||||
@get:Rule
|
||||
val rxRule = RxPluginsRule()
|
||||
|
||||
@get:Rule
|
||||
val appDependencies = MockAppDependenciesRule()
|
||||
|
||||
@get:Rule
|
||||
val iapRule = InAppPaymentsTestRule()
|
||||
|
||||
private val updateDispatcher = PublishProcessor.create<InAppPaymentTable.InAppPayment>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { InAppPaymentsRepository.observeUpdates(any()) } returns updateDispatcher
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a recurring PayPal donation, when I awaitTransaction, then I expect to add InAppPaymentPayPalRecurringSetupJob`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
verify {
|
||||
AppDependencies.jobManager.add(ofType(InAppPaymentPayPalRecurringSetupJob::class))
|
||||
}
|
||||
|
||||
test.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a recurring Stripe donation, when I awaitTransaction, then I expect to add InAppPaymentPayPalRecurringSetupJob`() {
|
||||
val inAppPayment = createInAppPayment(
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
)
|
||||
|
||||
val paymentSource = CreditCardPaymentSource(
|
||||
payload = JSONObject().apply {
|
||||
put("id", "token-id")
|
||||
}
|
||||
)
|
||||
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
verify {
|
||||
AppDependencies.jobManager.add(ofType(InAppPaymentStripeRecurringSetupJob::class))
|
||||
}
|
||||
|
||||
test.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a one time PayPal donation, when I awaitTransaction, then I expect to add InAppPaymentPayPalOneTimeSetupJob`() {
|
||||
val inAppPayment = createInAppPayment(
|
||||
type = InAppPaymentType.ONE_TIME_DONATION
|
||||
)
|
||||
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
verify {
|
||||
AppDependencies.jobManager.add(ofType(InAppPaymentPayPalOneTimeSetupJob::class))
|
||||
}
|
||||
|
||||
test.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a one time Stripe donation, when I awaitTransaction, then I expect to add InAppPaymentPayPalOneTimeSetupJob`() {
|
||||
val inAppPayment = createInAppPayment(
|
||||
type = InAppPaymentType.ONE_TIME_DONATION,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
)
|
||||
|
||||
val paymentSource = CreditCardPaymentSource(
|
||||
payload = JSONObject().apply {
|
||||
put("id", "token-id")
|
||||
}
|
||||
)
|
||||
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
verify {
|
||||
AppDependencies.jobManager.add(ofType(InAppPaymentStripeOneTimeSetupJob::class))
|
||||
}
|
||||
|
||||
test.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given END state with error, when I awaitTransaction, then I expect error`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
val errorPayment = inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.newBuilder().error(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING
|
||||
)
|
||||
).build()
|
||||
)
|
||||
|
||||
updateDispatcher.onNext(errorPayment)
|
||||
|
||||
test.assertError { throwable ->
|
||||
throwable is InAppPaymentError && throwable.inAppPaymentDataError == errorPayment.data.error
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given END state without error, when I awaitTransaction, then I expect error`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
val errorPayment = inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.END
|
||||
)
|
||||
|
||||
updateDispatcher.onNext(errorPayment)
|
||||
|
||||
test.assertError { throwable ->
|
||||
throwable is DonationError && throwable.source == inAppPayment.type.toErrorSource()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given REQUIRES_ACTION state, when I awaitTransaction, then I expect re-trigger`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
var wasCalled = false
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
wasCalled = true
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
val requiresAction = inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.REQUIRES_ACTION
|
||||
)
|
||||
|
||||
updateDispatcher.onNext(requiresAction)
|
||||
|
||||
assertThat(wasCalled).isTrue()
|
||||
|
||||
verify(exactly = 2) {
|
||||
AppDependencies.jobManager.add(ofType(InAppPaymentPayPalRecurringSetupJob::class))
|
||||
}
|
||||
|
||||
test.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given PENDING state transitions to END state without error, when I awaitTransaction, then I expect to complete`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.PENDING
|
||||
)
|
||||
)
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.END
|
||||
)
|
||||
)
|
||||
|
||||
test.assertComplete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given PENDING state transitions to END state with error, when I awaitTransaction, then I expect to complete`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.PENDING
|
||||
)
|
||||
)
|
||||
|
||||
val errorData = inAppPayment.data.newBuilder().error(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.REDEMPTION
|
||||
)
|
||||
).build()
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = errorData
|
||||
)
|
||||
)
|
||||
|
||||
test.assertError {
|
||||
it is InAppPaymentError && it.inAppPaymentDataError == errorData.error
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given PENDING state that times out, when I awaitTransaction, then I expect TimeoutWaitingForTokenError`() {
|
||||
val inAppPayment = createInAppPayment()
|
||||
val paymentSource = PayPalPaymentSource()
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.PENDING
|
||||
)
|
||||
)
|
||||
|
||||
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
|
||||
|
||||
test.assertError {
|
||||
it is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given long-running PENDING state that times out, when I awaitTransaction, then I expect DonationPending`() {
|
||||
val inAppPayment = createInAppPayment(
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.SEPA_DEBIT
|
||||
)
|
||||
|
||||
val paymentSource = SEPADebitPaymentSource(
|
||||
sepaDebitData = StripeApi.SEPADebitData("", "", "")
|
||||
)
|
||||
|
||||
val requiredActionHandler: RequiredActionHandler = {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
test.assertNoErrors()
|
||||
|
||||
updateDispatcher.onNext(
|
||||
inAppPayment.copy(
|
||||
state = InAppPaymentTable.State.PENDING
|
||||
)
|
||||
)
|
||||
|
||||
rxRule.defaultScheduler.advanceTimeBy(10, TimeUnit.SECONDS)
|
||||
|
||||
test.assertError {
|
||||
it is DonationError.BadgeRedemptionError.DonationPending
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInAppPayment(
|
||||
type: InAppPaymentType = InAppPaymentType.RECURRING_DONATION,
|
||||
paymentMethodType: InAppPaymentData.PaymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
): InAppPaymentTable.InAppPayment {
|
||||
return InAppPaymentTable.InAppPayment(
|
||||
id = InAppPaymentTable.InAppPaymentId(1L),
|
||||
type = type,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
insertedAt = 0.milliseconds,
|
||||
updatedAt = 0.milliseconds,
|
||||
notified = true,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 0.milliseconds,
|
||||
data = InAppPaymentData(
|
||||
paymentMethodType = paymentMethodType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user