ActiveSubscription state error unit tests for recurring job.

This commit is contained in:
Alex Hart
2025-01-29 10:53:43 -04:00
committed by Greyson Parrelli
parent fd1e47888a
commit c723bc812a
3 changed files with 361 additions and 12 deletions

View File

@@ -28,7 +28,9 @@ 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.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.util.JsonUtil
@@ -106,6 +108,9 @@ class InAppPaymentsTestRule : ExternalResource() {
every { SignalStore.Companion.inAppPayments } returns mockk {
every { setLastEndOfPeriod(any()) } returns Unit
}
every { SignalStore.Companion.backup } returns mockk {
every { hasBackupAlreadyRedeemedError = any() } returns Unit
}
}
override fun after() {
@@ -121,34 +126,42 @@ class InAppPaymentsTestRule : ExternalResource() {
}
fun initializeActiveSubscriptionMock(
status: Int = 200,
activeSubscription: ActiveSubscription? = null,
executionError: Throwable? = null,
applicationError: Throwable? = null
) {
every { AppDependencies.donationsService.getSubscription(any()) } returns ServiceResponse(200, "", activeSubscription, null, null)
every { AppDependencies.donationsService.getSubscription(any()) } returns ServiceResponse(status, "", activeSubscription, applicationError, executionError)
}
fun initializeSubmitReceiptCredentialRequestSync() {
val receiptCredentialResponse = mockk<ReceiptCredentialResponse>()
every { AppDependencies.donationsService.submitReceiptCredentialRequestSync(any(), any()) } returns ServiceResponse(200, "", receiptCredentialResponse, null, null)
fun initializeSubmitReceiptCredentialRequestSync(
status: Int = 200
) {
val receiptCredentialResponse = if (status == 200) mockk<ReceiptCredentialResponse>() else null
val applicationError = if (status == 200) null else NonSuccessfulResponseCodeException(status)
every { AppDependencies.donationsService.submitReceiptCredentialRequestSync(any(), any()) } returns ServiceResponse(status, "", receiptCredentialResponse, applicationError, null)
}
fun createActiveSubscription(): ActiveSubscription {
fun createActiveSubscription(
status: String = "active",
isActive: Boolean = true,
chargeFailure: ChargeFailure? = null
): ActiveSubscription {
return ActiveSubscription(
ActiveSubscription.Subscription(
2000,
"USD",
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 45.days.inWholeSeconds,
true,
isActive,
System.currentTimeMillis().milliseconds.inWholeSeconds + 45.days.inWholeSeconds,
false,
"active",
status,
"STRIPE",
"CARD",
false
),
null
chargeFailure
)
}
@@ -179,6 +192,12 @@ class InAppPaymentsTestRule : ExternalResource() {
val ref = AtomicReference(initialSubscriber)
every { InAppPaymentsRepository.getSubscriber(any()) } answers { ref.get() }
every { InAppPaymentsRepository.setSubscriber(any()) } answers { ref.set(firstArg()) }
every { SignalDatabase.inAppPaymentSubscribers.getBySubscriberId(any()) } answers {
ref.get()
}
every { SignalDatabase.inAppPaymentSubscribers.insertOrReplace(any()) } answers {
ref.set(firstArg())
}
return ref
}

View File

@@ -4,10 +4,12 @@ import android.app.Application
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isTrue
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -18,15 +20,20 @@ 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.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
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.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
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.ActiveSubscription.ChargeFailure
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@@ -109,7 +116,7 @@ class InAppPaymentRecurringContextJobTest {
@Test
fun `Test happy path for subscription redemption`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription)
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
@@ -199,19 +206,341 @@ class InAppPaymentRecurringContextJobTest {
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))
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = ActiveSubscription(null, null))
val result = job.run()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given getActiveSubscription app-level error, when I run, then I expect failure`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
inAppPaymentsTestRule.initializeActiveSubscriptionMock(applicationError = Exception())
val result = job.run()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given getActiveSubscription execution error, when I run, then I expect retry`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
inAppPaymentsTestRule.initializeActiveSubscriptionMock(executionError = Exception())
val result = job.run()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given a failed payment on a keep-alive, when I run, then I expect failure proper iap state`() {
val iap = insertInAppPayment(
redemptionState = InAppPaymentData.RedemptionState(
stage = InAppPaymentData.RedemptionState.Stage.INIT,
keepAlive = true
)
)
val job = InAppPaymentRecurringContextJob.create(iap)
val sub = inAppPaymentsTestRule.createActiveSubscription(
status = "past_due",
chargeFailure = null
)
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.error?.data_).isEqualTo(InAppPaymentKeepAliveJob.KEEP_ALIVE)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.PENDING)
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given a generic failed payment, when I run, then I expect properly updated state`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
val sub = inAppPaymentsTestRule.createActiveSubscription(
status = "past_due",
chargeFailure = null
)
InAppPaymentsTestRule.mockLocalSubscriberAccess()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.error?.data_).isEqualTo("past_due")
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(result.isFailure).isTrue()
val subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
assertThat(subscriber.requiresCancel).isTrue()
}
@Test
fun `Given a charge failure, when I run, then I expect properly updated state`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
val chargeFailure = ChargeFailure("test", "", "", "", "")
val sub = inAppPaymentsTestRule.createActiveSubscription(
status = "past_due",
chargeFailure = chargeFailure
)
InAppPaymentsTestRule.mockLocalSubscriberAccess()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.error?.data_).isEqualTo("test")
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(result.isFailure).isTrue()
val subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
assertThat(subscriber.requiresCancel).isTrue()
}
@Test
fun `Given an inactive subscription, when I run, then I retry`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
val sub = inAppPaymentsTestRule.createActiveSubscription(
isActive = false,
chargeFailure = null
)
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.error).isNull()
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.PENDING)
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given an inactive subscription with a charge failure, when I run, then I update state and fail`() {
val iap = insertInAppPayment()
val job = InAppPaymentRecurringContextJob.create(iap)
val chargeFailure = ChargeFailure("test", "", "", "", "")
val sub = inAppPaymentsTestRule.createActiveSubscription(
isActive = false,
chargeFailure = chargeFailure
)
InAppPaymentsTestRule.mockLocalSubscriberAccess()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.error?.data_).isEqualTo("test")
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given a canceled subscription with a charge failure for keep-alive, when I run, then I update state and fail`() {
val iap = insertInAppPayment(
redemptionState = InAppPaymentData.RedemptionState(
stage = InAppPaymentData.RedemptionState.Stage.INIT,
keepAlive = true
)
)
val job = InAppPaymentRecurringContextJob.create(iap)
val chargeFailure = ChargeFailure("test", "", "", "", "")
val sub = inAppPaymentsTestRule.createActiveSubscription(
status = "canceled",
isActive = false,
chargeFailure = chargeFailure
)
InAppPaymentsTestRule.mockLocalSubscriberAccess()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = sub)
val result = job.run()
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.data?.cancellation?.reason).isEqualTo(InAppPaymentData.Cancellation.Reason.CANCELED)
assertThat(updatedIap?.data?.cancellation?.chargeFailure).isEqualTo(chargeFailure.toInAppPaymentDataChargeFailure())
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given user has donor entitlement already, when I run, then I do not expect receipt request`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
every { AppDependencies.signalServiceAccountManager.whoAmI } returns mockk {
every { entitlements } returns WhoAmIResponse.Entitlements(
badges = listOf(WhoAmIResponse.BadgeEntitlement("2000", false, Long.MAX_VALUE))
)
}
val iap = insertInAppPayment(
badge = BadgeList.Badge(id = "2000")
)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isSuccess).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.donationsService.submitReceiptCredentialRequestSync(any(), any()) }
}
@Test
fun `Given user has backup entitlement already, when I run, then I do not expect receipt request`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
every { AppDependencies.signalServiceAccountManager.whoAmI } returns mockk {
every { entitlements } returns WhoAmIResponse.Entitlements(
backup = WhoAmIResponse.BackupEntitlement(201L, Long.MAX_VALUE)
)
}
val iap = insertInAppPayment(
type = InAppPaymentType.RECURRING_BACKUP
)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isSuccess).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.donationsService.submitReceiptCredentialRequestSync(any(), any()) }
}
@Test
fun `Given 204 application error, when I run, then I expect a retry`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 204)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isRetry).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.PENDING)
assertThat(updatedIap?.data?.error?.type).isNull()
}
@Test
fun `Given 400 application error, when I run, then I expect a terminal iap state`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 400)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isFailure).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(updatedIap?.data?.error?.type).isEqualTo(InAppPaymentData.Error.Type.REDEMPTION)
}
@Test
fun `Given 402 application error, when I run, then I expect a retry`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 402)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isRetry).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.PENDING)
assertThat(updatedIap?.data?.error?.type).isNull()
}
@Test
fun `Given 403 application error, when I run, then I expect a terminal iap state`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 403)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isFailure).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(updatedIap?.data?.error?.type).isEqualTo(InAppPaymentData.Error.Type.REDEMPTION)
}
@Test
fun `Given 404 application error, when I run, then I expect a terminal iap state`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 404)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isFailure).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(updatedIap?.data?.error?.type).isEqualTo(InAppPaymentData.Error.Type.REDEMPTION)
}
@Test
fun `Given 409 application error, when I run, then I expect a terminal iap state`() {
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription()
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
inAppPaymentsTestRule.initializeSubmitReceiptCredentialRequestSync(status = 409)
val iap = insertInAppPayment(paymentSourceType = PaymentSourceType.Stripe.IDEAL, type = InAppPaymentType.RECURRING_BACKUP)
val job = InAppPaymentRecurringContextJob.create(iap)
job.onAdded()
val result = job.run()
assertThat(result.isFailure).isTrue()
verify(atLeast = 0, atMost = 0) { AppDependencies.clientZkReceiptOperations.receiveReceiptCredential(any(), any()) }
val updatedIap = SignalDatabase.inAppPayments.getById(iap.id)
assertThat(updatedIap?.state).isEqualTo(InAppPaymentTable.State.END)
assertThat(updatedIap?.data?.error?.type).isEqualTo(InAppPaymentData.Error.Type.REDEMPTION)
assertThat(updatedIap?.data?.error?.data_).isEqualTo("409")
}
private fun insertInAppPayment(
type: InAppPaymentType = InAppPaymentType.RECURRING_DONATION,
state: InAppPaymentTable.State = InAppPaymentTable.State.CREATED,
subscriberId: SubscriberId? = SubscriberId.generate(),
paymentSourceType: PaymentSourceType = PaymentSourceType.Stripe.CreditCard,
badge: BadgeList.Badge? = null,
redemptionState: InAppPaymentData.RedemptionState? = InAppPaymentData.RedemptionState(
stage = InAppPaymentData.RedemptionState.Stage.INIT
stage = InAppPaymentData.RedemptionState.Stage.INIT,
keepAlive = false
)
): InAppPaymentTable.InAppPayment {
val iap = inAppPaymentsTestRule.createInAppPayment(type, paymentSourceType)
@@ -221,6 +550,7 @@ class InAppPaymentRecurringContextJobTest {
subscriberId = subscriberId,
endOfPeriod = null,
inAppPaymentData = iap.data.copy(
badge = badge,
redemption = redemptionState
)
)