From 257b8695e6c0ccef0baa36f75c439556c46bcc88 Mon Sep 17 00:00:00 2001 From: toluo-stripe Date: Mon, 10 Feb 2025 13:34:13 -0500 Subject: [PATCH] Abstract Link Attestation Check (#10103) --- .../payments/core/analytics/ErrorReporter.kt | 6 +- .../android/link/LinkActivityViewModel.kt | 68 +----- .../DefaultLinkAttestationCheck.kt | 61 +++++ .../link/attestation/LinkAttestationCheck.kt | 12 + .../link/injection/ApplicationIdModule.kt | 17 ++ .../IntegrityRequestManagerModule.kt | 24 ++ .../link/injection/LinkViewModelModule.kt | 15 +- .../link/injection/NativeLinkComponent.kt | 2 + .../link/injection/NativeLinkModule.kt | 31 +-- .../stripe/android/link/LinkActivityTest.kt | 9 +- .../android/link/LinkActivityViewModelTest.kt | 225 +----------------- .../DefaultLinkAttestationCheckTest.kt | 162 +++++++++++++ .../attestation/FakeLinkAttestationCheck.kt | 21 ++ 13 files changed, 338 insertions(+), 315 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheck.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/attestation/LinkAttestationCheck.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/injection/ApplicationIdModule.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/link/injection/IntegrityRequestManagerModule.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheckTest.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/link/attestation/FakeLinkAttestationCheck.kt diff --git a/payments-core/src/main/java/com/stripe/android/payments/core/analytics/ErrorReporter.kt b/payments-core/src/main/java/com/stripe/android/payments/core/analytics/ErrorReporter.kt index 9cd1fa402c6..4713779d1b1 100644 --- a/payments-core/src/main/java/com/stripe/android/payments/core/analytics/ErrorReporter.kt +++ b/payments-core/src/main/java/com/stripe/android/payments/core/analytics/ErrorReporter.kt @@ -131,6 +131,9 @@ interface ErrorReporter : FraudDetectionErrorReporter { LINK_LOG_OUT_FAILURE( eventName = "link.log_out.failure" ), + LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER( + eventName = "link.native.integrity.preparation_failed" + ), PAYMENT_LAUNCHER_CONFIRMATION_NULL_ARGS( eventName = "payments.paymentlauncherconfirmation.null_args" ), @@ -195,9 +198,6 @@ interface ErrorReporter : FraudDetectionErrorReporter { LINK_WEB_FAILED_TO_PARSE_RESULT_URI( partialEventName = "link.web.result.parsing_failed" ), - LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER( - partialEventName = "link.native.integrity.preparation_failed" - ), LINK_NATIVE_FAILED_TO_ATTEST_REQUEST( partialEventName = "link.native.failed_to_attest_request" ), diff --git a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt index 6147ff50e03..0fe07a1f2bb 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/LinkActivityViewModel.kt @@ -15,21 +15,16 @@ import androidx.lifecycle.viewmodel.viewModelFactory import androidx.navigation.NavHostController import com.stripe.android.link.LinkActivity.Companion.getArgs import com.stripe.android.link.account.LinkAccountManager -import com.stripe.android.link.account.LinkAuth -import com.stripe.android.link.account.LinkAuthResult import com.stripe.android.link.account.linkAccountUpdate -import com.stripe.android.link.gate.LinkGate +import com.stripe.android.link.attestation.LinkAttestationCheck import com.stripe.android.link.injection.DaggerNativeLinkComponent import com.stripe.android.link.injection.NativeLinkComponent import com.stripe.android.link.model.AccountStatus import com.stripe.android.link.model.LinkAccount import com.stripe.android.link.ui.LinkAppBarState -import com.stripe.android.model.EmailSource import com.stripe.android.paymentelement.confirmation.ConfirmationHandler -import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.analytics.EventReporter -import com.stripe.attestation.IntegrityRequestManager import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow @@ -45,11 +40,8 @@ internal class LinkActivityViewModel @Inject constructor( confirmationHandlerFactory: ConfirmationHandler.Factory, private val linkAccountManager: LinkAccountManager, val eventReporter: EventReporter, - private val integrityRequestManager: IntegrityRequestManager, - private val linkGate: LinkGate, - private val errorReporter: ErrorReporter, - private val linkAuth: LinkAuth, private val linkConfiguration: LinkConfiguration, + private val linkAttestationCheck: LinkAttestationCheck, private val startWithVerificationDialog: Boolean ) : ViewModel(), DefaultLifecycleObserver { val confirmationHandler = confirmationHandlerFactory.create(viewModelScope) @@ -192,14 +184,19 @@ internal class LinkActivityViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { - performAttestationCheck().fold( - onSuccess = { - updateScreenState() - }, - onFailure = { + val attestationCheckResult = linkAttestationCheck.invoke() + when (attestationCheckResult) { + is LinkAttestationCheck.Result.AttestationFailed -> { moveToWeb() } - ) + LinkAttestationCheck.Result.Successful -> { + updateScreenState() + } + is LinkAttestationCheck.Result.AccountError, + is LinkAttestationCheck.Result.Error -> { + // Display error screen here like web + } + } } } @@ -209,33 +206,6 @@ internal class LinkActivityViewModel @Inject constructor( } } - private suspend fun performAttestationCheck(): Result { - if (linkGate.useAttestationEndpoints.not()) return Result.success(Unit) - return integrityRequestManager.prepare() - .onFailure { error -> - errorReporter.report( - errorEvent = ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER, - stripeException = LinkEventException(error) - ) - } - .mapCatching { - when (val lookupResult = lookupUser()) { - is LinkAuthResult.AttestationFailed -> { - errorReporter.report( - errorEvent = ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST, - stripeException = LinkEventException(lookupResult.error) - ) - throw lookupResult.error - } - is LinkAuthResult.Error, - is LinkAuthResult.AccountError, - LinkAuthResult.NoLinkAccountFound, - is LinkAuthResult.Success, - null -> Unit - } - } - } - private suspend fun updateScreenState() { val accountStatus = linkAccountManager.accountStatus.first() val linkAccount = linkAccountManager.linkAccount.value @@ -272,18 +242,6 @@ internal class LinkActivityViewModel @Inject constructor( navigate(screen, clearStack = true, launchSingleTop = true) } - private suspend fun lookupUser(): LinkAuthResult? { - val customerEmail = linkAccountManager.linkAccount.value?.email - ?: linkConfiguration.customerInfo.email - ?: return null - - return linkAuth.lookUp( - email = customerEmail, - emailSource = EmailSource.CUSTOMER_OBJECT, - startSession = false - ) - } - companion object { private val showHeaderRoutes = setOf( LinkScreen.Wallet.route, diff --git a/paymentsheet/src/main/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheck.kt b/paymentsheet/src/main/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheck.kt new file mode 100644 index 00000000000..325022cb4a1 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheck.kt @@ -0,0 +1,61 @@ +package com.stripe.android.link.attestation + +import com.stripe.android.core.exception.StripeException +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.account.LinkAccountManager +import com.stripe.android.link.account.LinkAuth +import com.stripe.android.link.account.LinkAuthResult +import com.stripe.android.link.gate.LinkGate +import com.stripe.android.model.EmailSource +import com.stripe.android.payments.core.analytics.ErrorReporter +import com.stripe.attestation.IntegrityRequestManager +import javax.inject.Inject + +internal class DefaultLinkAttestationCheck @Inject constructor( + private val linkGate: LinkGate, + private val linkAuth: LinkAuth, + private val integrityRequestManager: IntegrityRequestManager, + private val linkAccountManager: LinkAccountManager, + private val linkConfiguration: LinkConfiguration, + private val errorReporter: ErrorReporter +) : LinkAttestationCheck { + override suspend fun invoke(): LinkAttestationCheck.Result { + if (linkGate.useAttestationEndpoints.not()) return LinkAttestationCheck.Result.Successful + val result = integrityRequestManager.prepare() + + return result.fold( + onSuccess = { + val email = linkAccountManager.linkAccount.value?.email + ?: linkConfiguration.customerInfo.email + if (email == null) return@fold LinkAttestationCheck.Result.Successful + val lookupResult = linkAuth.lookUp( + email = email, + emailSource = EmailSource.CUSTOMER_OBJECT, + startSession = false + ) + when (lookupResult) { + is LinkAuthResult.AttestationFailed -> { + LinkAttestationCheck.Result.AttestationFailed(lookupResult.error) + } + is LinkAuthResult.Error -> { + LinkAttestationCheck.Result.Error(lookupResult.error) + } + is LinkAuthResult.AccountError -> { + LinkAttestationCheck.Result.AccountError(lookupResult.error) + } + LinkAuthResult.NoLinkAccountFound, + is LinkAuthResult.Success -> { + LinkAttestationCheck.Result.Successful + } + } + }, + onFailure = { error -> + errorReporter.report( + errorEvent = ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER, + stripeException = StripeException.create(error) + ) + LinkAttestationCheck.Result.AttestationFailed(error) + } + ) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/attestation/LinkAttestationCheck.kt b/paymentsheet/src/main/java/com/stripe/android/link/attestation/LinkAttestationCheck.kt new file mode 100644 index 00000000000..62ced0ff32d --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/attestation/LinkAttestationCheck.kt @@ -0,0 +1,12 @@ +package com.stripe.android.link.attestation + +internal interface LinkAttestationCheck { + suspend fun invoke(): Result + + sealed interface Result { + data object Successful : Result + data class AttestationFailed(val error: Throwable) : Result + data class AccountError(val error: Throwable) : Result + data class Error(val error: Throwable) : Result + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/ApplicationIdModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/ApplicationIdModule.kt new file mode 100644 index 00000000000..117df3cec2b --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/ApplicationIdModule.kt @@ -0,0 +1,17 @@ +package com.stripe.android.link.injection + +import android.app.Application +import dagger.Module +import dagger.Provides +import javax.inject.Named + +@Module +internal object ApplicationIdModule { + @Provides + @Named(APPLICATION_ID) + fun provideApplicationId( + application: Application + ): String { + return application.packageName + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/IntegrityRequestManagerModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/IntegrityRequestManagerModule.kt new file mode 100644 index 00000000000..6dedf947013 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/IntegrityRequestManagerModule.kt @@ -0,0 +1,24 @@ +package com.stripe.android.link.injection + +import android.app.Application +import com.stripe.android.BuildConfig +import com.stripe.android.core.Logger +import com.stripe.attestation.IntegrityRequestManager +import com.stripe.attestation.IntegrityStandardRequestManager +import com.stripe.attestation.RealStandardIntegrityManagerFactory +import dagger.Module +import dagger.Provides + +@Module +internal object IntegrityRequestManagerModule { + @Provides + fun providesIntegrityStandardRequestManager( + context: Application + ): IntegrityRequestManager = IntegrityStandardRequestManager( + cloudProjectNumber = 577365562050, // stripe-payments-sdk-prod + logError = { message, error -> + Logger.getInstance(BuildConfig.DEBUG).error(message, error) + }, + factory = RealStandardIntegrityManagerFactory(context) + ) +} diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt index 2e25ae9a64a..95be1ff7d96 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/LinkViewModelModule.kt @@ -3,12 +3,9 @@ package com.stripe.android.link.injection import com.stripe.android.link.LinkActivityViewModel import com.stripe.android.link.LinkConfiguration import com.stripe.android.link.account.LinkAccountManager -import com.stripe.android.link.account.LinkAuth -import com.stripe.android.link.gate.LinkGate +import com.stripe.android.link.attestation.LinkAttestationCheck import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler -import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.paymentsheet.analytics.EventReporter -import com.stripe.attestation.IntegrityRequestManager import dagger.Module import dagger.Provides import javax.inject.Named @@ -22,11 +19,8 @@ internal object LinkViewModelModule { defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory, linkAccountManager: LinkAccountManager, eventReporter: EventReporter, - integrityRequestManager: IntegrityRequestManager, - linkGate: LinkGate, - errorReporter: ErrorReporter, - linkAuth: LinkAuth, linkConfiguration: LinkConfiguration, + linkAttestationCheck: LinkAttestationCheck, @Named(START_WITH_VERIFICATION_DIALOG) startWithVerificationDialog: Boolean ): LinkActivityViewModel { return LinkActivityViewModel( @@ -34,11 +28,8 @@ internal object LinkViewModelModule { confirmationHandlerFactory = defaultConfirmationHandlerFactory, linkAccountManager = linkAccountManager, eventReporter = eventReporter, - integrityRequestManager = integrityRequestManager, - linkGate = linkGate, - errorReporter = errorReporter, - linkAuth = linkAuth, linkConfiguration = linkConfiguration, + linkAttestationCheck = linkAttestationCheck, startWithVerificationDialog = startWithVerificationDialog ) } diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt index d883535c755..a3dd8d4f12e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkComponent.kt @@ -32,6 +32,8 @@ internal annotation class NativeLinkScope modules = [ NativeLinkModule::class, LinkViewModelModule::class, + IntegrityRequestManagerModule::class, + ApplicationIdModule::class, DefaultConfirmationModule::class, ] ) diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt index 7dbaaa05b9f..dc63717bfbc 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt @@ -1,6 +1,5 @@ package com.stripe.android.link.injection -import android.app.Application import android.content.Context import androidx.core.os.LocaleListCompat import androidx.lifecycle.SavedStateHandle @@ -29,6 +28,8 @@ import com.stripe.android.link.account.LinkAccountManager import com.stripe.android.link.account.LinkAuth import com.stripe.android.link.analytics.DefaultLinkEventsReporter import com.stripe.android.link.analytics.LinkEventsReporter +import com.stripe.android.link.attestation.DefaultLinkAttestationCheck +import com.stripe.android.link.attestation.LinkAttestationCheck import com.stripe.android.link.confirmation.DefaultLinkConfirmationHandler import com.stripe.android.link.confirmation.LinkConfirmationHandler import com.stripe.android.link.gate.DefaultLinkGate @@ -46,9 +47,6 @@ import com.stripe.android.paymentsheet.analytics.DefaultEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter import com.stripe.android.repository.ConsumersApiService import com.stripe.android.repository.ConsumersApiServiceImpl -import com.stripe.attestation.IntegrityRequestManager -import com.stripe.attestation.IntegrityStandardRequestManager -import com.stripe.attestation.RealStandardIntegrityManagerFactory import dagger.Binds import dagger.Module import dagger.Provides @@ -96,6 +94,10 @@ internal interface NativeLinkModule { @NativeLinkScope fun bindsLinkAuth(linkGate: DefaultLinkAuth): LinkAuth + @Binds + @NativeLinkScope + fun bindsLinkAttestationCheck(linkAttestationCheck: DefaultLinkAttestationCheck): LinkAttestationCheck + @SuppressWarnings("TooManyFunctions") companion object { @Provides @@ -191,29 +193,8 @@ internal interface NativeLinkModule { factory: DefaultLinkConfirmationHandler.Factory ): LinkConfirmationHandler.Factory = factory - @Provides - @NativeLinkScope - fun providesIntegrityStandardRequestManager( - context: Application - ): IntegrityRequestManager = IntegrityStandardRequestManager( - cloudProjectNumber = 577365562050, // stripe-payments-sdk-prod - logError = { message, error -> - Logger.getInstance(BuildConfig.DEBUG).error(message, error) - }, - factory = RealStandardIntegrityManagerFactory(context) - ) - @Provides @NativeLinkScope fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom - - @Provides - @NativeLinkScope - @Named(APPLICATION_ID) - fun provideApplicationId( - application: Application - ): String { - return application.packageName - } } } diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityTest.kt index 562d1f8f416..391c7793951 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityTest.kt @@ -16,14 +16,12 @@ import androidx.test.espresso.intent.Intents.assertNoUnverifiedIntents import androidx.test.espresso.intent.rule.IntentsRule import com.google.common.truth.Truth.assertThat import com.stripe.android.link.account.FakeLinkAccountManager -import com.stripe.android.link.account.FakeLinkAuth import com.stripe.android.link.account.LinkAccountManager -import com.stripe.android.link.gate.FakeLinkGate +import com.stripe.android.link.attestation.FakeLinkAttestationCheck import com.stripe.android.link.model.AccountStatus import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler import com.stripe.android.paymentsheet.analytics.FakeEventReporter import com.stripe.android.testing.CoroutineTestRule -import com.stripe.android.testing.FakeErrorReporter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain @@ -146,10 +144,7 @@ internal class LinkActivityTest { confirmationHandlerFactory = { FakeConfirmationHandler() }, linkAccountManager = linkAccountManager, eventReporter = FakeEventReporter(), - integrityRequestManager = FakeIntegrityRequestManager(), - linkGate = FakeLinkGate(), - errorReporter = FakeErrorReporter(), - linkAuth = FakeLinkAuth(), + linkAttestationCheck = FakeLinkAttestationCheck(), linkConfiguration = TestFactory.LINK_CONFIGURATION, startWithVerificationDialog = use2faDialog, ) diff --git a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt index cde7ce64065..abc7f7e4529 100644 --- a/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/link/LinkActivityViewModelTest.kt @@ -22,23 +22,16 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.stripe.android.link.account.FakeLinkAccountManager -import com.stripe.android.link.account.FakeLinkAuth -import com.stripe.android.link.account.LinkAuth -import com.stripe.android.link.account.LinkAuthResult -import com.stripe.android.link.gate.FakeLinkGate -import com.stripe.android.link.gate.LinkGate +import com.stripe.android.link.attestation.FakeLinkAttestationCheck +import com.stripe.android.link.attestation.LinkAttestationCheck import com.stripe.android.link.model.AccountStatus -import com.stripe.android.link.model.LinkAccount import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler -import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.paymentsheet.R import com.stripe.android.paymentsheet.analytics.EventReporter import com.stripe.android.paymentsheet.analytics.FakeEventReporter import com.stripe.android.testing.CoroutineTestRule -import com.stripe.android.testing.FakeErrorReporter import com.stripe.android.utils.DummyActivityResultCaller -import com.stripe.attestation.IntegrityRequestManager import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle @@ -327,34 +320,25 @@ internal class LinkActivityViewModelTest { } @Test - fun `onCreate should launch web when attestation fails and useAttestationEndpoints is enabled`() = runTest { + fun `onCreate should launch web when attestation check fails`() = runTest { var launchWebConfig: LinkConfiguration? = null - val linkAccountManager = FakeLinkAccountManager() val navController = navController() - val linkGate = FakeLinkGate() - val integrityRequestManager = FakeIntegrityRequestManager() - val errorReporter = FakeErrorReporter() - - integrityRequestManager.prepareResult = Result.failure(Throwable("oops")) - linkGate.setUseAttestationEndpoints(true) + val error = Throwable("oops") + val linkAttestationCheck = FakeLinkAttestationCheck() + linkAttestationCheck.result = LinkAttestationCheck.Result.AttestationFailed(error) val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkGate = linkGate, - integrityRequestManager = integrityRequestManager, - errorReporter = errorReporter, + linkAttestationCheck = linkAttestationCheck, launchWeb = { config -> launchWebConfig = config } ) vm.navController = navController - linkAccountManager.setAccountStatus(AccountStatus.Verified) vm.onCreate(mock()) advanceUntilIdle() - integrityRequestManager.awaitPrepareCall() assertNavigation( navController = navController, screen = LinkScreen.Loading, @@ -362,211 +346,39 @@ internal class LinkActivityViewModelTest { launchSingleTop = false ) assertThat(launchWebConfig).isNotNull() - assertThat(errorReporter.getLoggedErrors()) - .containsExactly( - ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER.eventName - ) - integrityRequestManager.ensureAllEventsConsumed() } @Test - fun `onCreate shouldn't launch web when integrity preparation passes and useAttestationEndpoints is enabled`() = + fun `onCreate shouldn't launch web when attestationCheck fails`() = runTest { var launchWebConfig: LinkConfiguration? = null - val linkAccountManager = FakeLinkAccountManager() val navController = navController() - val linkGate = FakeLinkGate() - val integrityRequestManager = FakeIntegrityRequestManager() - - linkGate.setUseAttestationEndpoints(true) + val linkAttestationCheck = FakeLinkAttestationCheck() val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkGate = linkGate, - integrityRequestManager = integrityRequestManager, + linkAttestationCheck = linkAttestationCheck, launchWeb = { config -> launchWebConfig = config } ) vm.navController = navController - linkAccountManager.setAccountStatus(AccountStatus.Verified) vm.onCreate(mock()) advanceUntilIdle() - integrityRequestManager.awaitPrepareCall() assertThat(vm.linkScreenState.value).isEqualTo(ScreenState.FullScreen) assertThat(launchWebConfig).isNull() - integrityRequestManager.ensureAllEventsConsumed() } - @Test - fun `onCreate should not prepare integrity when useAttestationEndpoints is disabled`() = runTest { - var launchWebConfig: LinkConfiguration? = null - val linkAccountManager = FakeLinkAccountManager() - val navController = navController() - val linkGate = FakeLinkGate() - val integrityRequestManager = FakeIntegrityRequestManager() - - linkGate.setUseAttestationEndpoints(false) - - val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkGate = linkGate, - integrityRequestManager = integrityRequestManager, - launchWeb = { config -> - launchWebConfig = config - } - ) - vm.navController = navController - linkAccountManager.setAccountStatus(AccountStatus.Verified) - - vm.onCreate(mock()) - - advanceUntilIdle() - - assertThat(launchWebConfig).isNull() - integrityRequestManager.ensureAllEventsConsumed() - } - - @Test - fun `onCreate should launch web on lookup attestation error`() = runTest { - val error = Throwable("oops") - var launchWebConfig: LinkConfiguration? = null - val linkAccountManager = FakeLinkAccountManager() - val navController = navController() - val linkAuth = FakeLinkAuth() - val errorReporter = FakeErrorReporter() - - val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkAuth = linkAuth, - errorReporter = errorReporter, - launchWeb = { config -> - launchWebConfig = config - } - ) - vm.navController = navController - linkAccountManager.setAccountStatus(AccountStatus.Verified) - linkAuth.lookupResult = LinkAuthResult.AttestationFailed(error) - - vm.onCreate(mock()) - - advanceUntilIdle() - - linkAuth.awaitLookupCall() - - assertNavigation( - navController = navController, - screen = LinkScreen.Loading, - clearStack = true, - launchSingleTop = false - ) - assertThat(launchWebConfig).isNotNull() - assertThat(errorReporter.getLoggedErrors()) - .containsExactly( - ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST.eventName - ) - linkAuth.ensureAllItemsConsumed() - } - - @Test - fun `onCreate should not launch web on generic lookup error`() = runTest { - val error = Throwable("oops") - var launchWebConfig: LinkConfiguration? = null - val linkAccountManager = FakeLinkAccountManager() - val navController = navController() - val linkAuth = FakeLinkAuth() - - val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkAuth = linkAuth, - launchWeb = { config -> - launchWebConfig = config - } - ) - vm.navController = navController - linkAccountManager.setAccountStatus(AccountStatus.Verified) - linkAuth.lookupResult = LinkAuthResult.Error(error) - - vm.onCreate(mock()) - - advanceUntilIdle() - - linkAuth.awaitLookupCall() - - assertThat(vm.linkScreenState.value).isEqualTo(ScreenState.FullScreen) - assertThat(launchWebConfig).isNull() - linkAuth.ensureAllItemsConsumed() - } - - @Test - fun `onCreate should lookup with link account email when signed in`() = runTest { - val linkAccountManager = FakeLinkAccountManager() - val linkGate = FakeLinkGate() - val linkAuth = FakeLinkAuth() - - linkAccountManager.setLinkAccount( - account = LinkAccount( - consumerSession = TestFactory.CONSUMER_SESSION.copy( - emailAddress = "linkaccountmanager@email.com" - ) - ) - ) - linkGate.setUseAttestationEndpoints(true) - - val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkGate = linkGate, - linkAuth = linkAuth, - launchWeb = {} - ) - - vm.onCreate(mock()) - - advanceUntilIdle() - - val call = linkAuth.awaitLookupCall() - - assertThat(call.email).isEqualTo("linkaccountmanager@email.com") - } - - @Test - fun `onCreate should lookup with configuration email when not signed in`() = runTest { - val linkAccountManager = FakeLinkAccountManager() - val linkGate = FakeLinkGate() - val linkAuth = FakeLinkAuth() - - linkAccountManager.setLinkAccount(null) - linkGate.setUseAttestationEndpoints(true) - - val vm = createViewModel( - linkAccountManager = linkAccountManager, - linkGate = linkGate, - linkAuth = linkAuth, - launchWeb = {} - ) - - vm.onCreate(mock()) - - advanceUntilIdle() - - val call = linkAuth.awaitLookupCall() - - assertThat(call.email).isEqualTo(TestFactory.LINK_CONFIGURATION.customerInfo.email) - } - @Test fun `onCreate should launch 2fa when eager launch is enabled`() = runTest { val linkAccountManager = FakeLinkAccountManager() linkAccountManager.setLinkAccount(TestFactory.LINK_ACCOUNT) val navController = navController() - val linkAuth = FakeLinkAuth() val vm = createViewModel( linkAccountManager = linkAccountManager, - linkAuth = linkAuth, startWithVerificationDialog = true ) vm.navController = navController @@ -576,10 +388,7 @@ internal class LinkActivityViewModelTest { advanceUntilIdle() - linkAuth.awaitLookupCall() - assertThat(vm.linkScreenState.value).isEqualTo(ScreenState.VerificationDialog(TestFactory.LINK_ACCOUNT)) - linkAuth.ensureAllItemsConsumed() } @Test @@ -587,11 +396,9 @@ internal class LinkActivityViewModelTest { val linkAccountManager = FakeLinkAccountManager() linkAccountManager.setLinkAccount(TestFactory.LINK_ACCOUNT) val navController = navController() - val linkAuth = FakeLinkAuth() val vm = createViewModel( linkAccountManager = linkAccountManager, - linkAuth = linkAuth, startWithVerificationDialog = true ) vm.navController = navController @@ -613,12 +420,10 @@ internal class LinkActivityViewModelTest { val linkAccountManager = FakeLinkAccountManager() linkAccountManager.setLinkAccount(TestFactory.LINK_ACCOUNT) val navController = navController() - val linkAuth = FakeLinkAuth() var activityResult: LinkActivityResult? = null val vm = createViewModel( linkAccountManager = linkAccountManager, - linkAuth = linkAuth, startWithVerificationDialog = true, dismissWithResult = { activityResult = it @@ -806,10 +611,7 @@ internal class LinkActivityViewModelTest { confirmationHandler: ConfirmationHandler = FakeConfirmationHandler(), eventReporter: EventReporter = FakeEventReporter(), navController: NavHostController = navController(), - integrityRequestManager: IntegrityRequestManager = FakeIntegrityRequestManager(), - linkGate: LinkGate = FakeLinkGate(), - errorReporter: ErrorReporter = FakeErrorReporter(), - linkAuth: LinkAuth = FakeLinkAuth(), + linkAttestationCheck: LinkAttestationCheck = FakeLinkAttestationCheck(), startWithVerificationDialog: Boolean = false, dismissWithResult: (LinkActivityResult) -> Unit = {}, launchWeb: (LinkConfiguration) -> Unit = {} @@ -819,10 +621,7 @@ internal class LinkActivityViewModelTest { activityRetainedComponent = FakeNativeLinkComponent(), eventReporter = eventReporter, confirmationHandlerFactory = { confirmationHandler }, - integrityRequestManager = integrityRequestManager, - linkGate = linkGate, - errorReporter = errorReporter, - linkAuth = linkAuth, + linkAttestationCheck = linkAttestationCheck, linkConfiguration = TestFactory.LINK_CONFIGURATION, startWithVerificationDialog = startWithVerificationDialog, ).apply { diff --git a/paymentsheet/src/test/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheckTest.kt b/paymentsheet/src/test/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheckTest.kt new file mode 100644 index 00000000000..43a64887b15 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/link/attestation/DefaultLinkAttestationCheckTest.kt @@ -0,0 +1,162 @@ +package com.stripe.android.link.attestation + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.link.FakeIntegrityRequestManager +import com.stripe.android.link.LinkConfiguration +import com.stripe.android.link.TestFactory +import com.stripe.android.link.account.FakeLinkAccountManager +import com.stripe.android.link.account.FakeLinkAuth +import com.stripe.android.link.account.LinkAccountManager +import com.stripe.android.link.account.LinkAuth +import com.stripe.android.link.account.LinkAuthResult +import com.stripe.android.link.gate.FakeLinkGate +import com.stripe.android.link.gate.LinkGate +import com.stripe.android.payments.core.analytics.ErrorReporter +import com.stripe.android.testing.CoroutineTestRule +import com.stripe.android.testing.FakeErrorReporter +import com.stripe.attestation.IntegrityRequestManager +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +internal class DefaultLinkAttestationCheckTest { + @get:Rule + val testRule = CoroutineTestRule() + + @Test + fun `attestation check should be successful when useAttestationEndpoints is false`() = runTest { + val linkGate = FakeLinkGate() + linkGate.setUseAttestationEndpoints(false) + + val attestationCheck = attestationCheck(linkGate = linkGate) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.Successful) + } + + @Test + fun `attestation check should be successful when there is no email for lookup`() = runTest { + val linkGate = FakeLinkGate() + val linkAccountManager = FakeLinkAccountManager() + linkGate.setUseAttestationEndpoints(true) + linkAccountManager.setLinkAccount(null) + + val attestationCheck = attestationCheck( + linkGate = linkGate, + linkAccountManager = linkAccountManager, + linkConfiguration = TestFactory.LINK_CONFIGURATION.copy( + customerInfo = TestFactory.LINK_CUSTOMER_INFO.copy( + email = null + ) + ) + ) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.Successful) + } + + @Test + fun `attestation check should return AttestationFailed when integrity preparation fails`() = runTest { + val error = Throwable("oops") + val errorReporter = FakeErrorReporter() + val integrityRequestManager = FakeIntegrityRequestManager() + integrityRequestManager.prepareResult = Result.failure(error) + + val attestationCheck = attestationCheck( + integrityRequestManager = integrityRequestManager, + errorReporter = errorReporter + ) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.AttestationFailed(error)) + assertThat(errorReporter.getLoggedErrors()) + .containsExactly( + ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER.eventName + ) + } + + @Test + fun `attestation check should return AttestationFailed when lookup returns AttestationFailed`() = runTest { + val error = Throwable("oops") + val linkGate = FakeLinkGate() + val linkAuth = FakeLinkAuth() + + linkAuth.lookupResult = LinkAuthResult.AttestationFailed(error) + + val attestationCheck = attestationCheck( + linkGate = linkGate, + linkAuth = linkAuth, + ) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.AttestationFailed(error)) + } + + @Test + fun `attestation check should return AccountError when lookup returns AccountError`() = runTest { + val error = Throwable("oops") + val linkGate = FakeLinkGate() + val linkAuth = FakeLinkAuth() + linkAuth.lookupResult = LinkAuthResult.AccountError(error) + + val attestationCheck = attestationCheck(linkGate = linkGate, linkAuth = linkAuth) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.AccountError(error)) + } + + @Test + fun `attestation check should return Successful when lookup returns NoLinkAccountFound`() = runTest { + val linkGate = FakeLinkGate() + val linkAuth = FakeLinkAuth() + linkAuth.lookupResult = LinkAuthResult.NoLinkAccountFound + + val attestationCheck = attestationCheck(linkGate = linkGate, linkAuth = linkAuth) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.Successful) + } + + @Test + fun `attestation check should return Successful when lookup returns success`() = runTest { + val linkGate = FakeLinkGate() + val linkAuth = FakeLinkAuth() + linkAuth.lookupResult = LinkAuthResult.Success(TestFactory.LINK_ACCOUNT) + + val attestationCheck = attestationCheck(linkGate = linkGate, linkAuth = linkAuth) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.Successful) + } + + @Test + fun `attestation check should return Error when lookup returns error`() = runTest { + val error = Throwable("oops") + val linkGate = FakeLinkGate() + val linkAuth = FakeLinkAuth() + linkAuth.lookupResult = LinkAuthResult.Error(error) + + val attestationCheck = attestationCheck(linkGate = linkGate, linkAuth = linkAuth) + + assertThat(attestationCheck.invoke()) + .isEqualTo(LinkAttestationCheck.Result.Error(error)) + } + + private fun attestationCheck( + linkGate: LinkGate = FakeLinkGate(), + linkAuth: LinkAuth = FakeLinkAuth(), + integrityRequestManager: IntegrityRequestManager = FakeIntegrityRequestManager(), + linkAccountManager: LinkAccountManager = FakeLinkAccountManager(), + errorReporter: ErrorReporter = FakeErrorReporter(), + linkConfiguration: LinkConfiguration = TestFactory.LINK_CONFIGURATION + ): DefaultLinkAttestationCheck { + return DefaultLinkAttestationCheck( + linkGate = linkGate, + linkAuth = linkAuth, + integrityRequestManager = integrityRequestManager, + linkAccountManager = linkAccountManager, + linkConfiguration = linkConfiguration, + errorReporter = errorReporter + ) + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/link/attestation/FakeLinkAttestationCheck.kt b/paymentsheet/src/test/java/com/stripe/android/link/attestation/FakeLinkAttestationCheck.kt new file mode 100644 index 00000000000..f882ad49d57 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/link/attestation/FakeLinkAttestationCheck.kt @@ -0,0 +1,21 @@ +package com.stripe.android.link.attestation + +import app.cash.turbine.Turbine + +internal class FakeLinkAttestationCheck : LinkAttestationCheck { + var result: LinkAttestationCheck.Result = LinkAttestationCheck.Result.Successful + private val calls = Turbine() + + override suspend fun invoke(): LinkAttestationCheck.Result { + calls.add(Unit) + return result + } + + suspend fun awaitInvokeCall() { + calls.awaitItem() + } + + fun ensureAllEventsConsumed() { + calls.ensureAllEventsConsumed() + } +}