Updated on 2026-08-14
This commit is contained in:
parent
4613836a1b
commit
6a82ee1600
7 changed files with 1715 additions and 2 deletions
|
|
@ -11,6 +11,10 @@ android {
|
|||
namespace = "com.tangem.features.details.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project - API */
|
||||
|
|
@ -80,4 +84,11 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
|
||||
/* Test */
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.coroutine)
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
package com.tangem.features.details.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.details.impl.R
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class UserWalletSaverTest {
|
||||
|
||||
private val scanCardProcessor: ScanCardProcessor = mockk()
|
||||
private val saveWalletUseCase: SaveWalletUseCase = mockk()
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory = mockk()
|
||||
private val messageSender: UiMessageSender = mockk(relaxUnitFun = true)
|
||||
private val router: Router = mockk(relaxUnitFun = true)
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles = mockk()
|
||||
|
||||
private val scanResponse: ScanResponse = mockk()
|
||||
private val userWalletId: UserWalletId = UserWalletId("011")
|
||||
private val userWallet: UserWallet.Cold = mockk {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onWalletNotCreated WHEN scanAndSaveUserWallet THEN no message AND no save`() = runTest {
|
||||
mockScanCallback(callbackName = ON_WALLET_NOT_CREATED)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify(exactly = 0) { messageSender.send(any()) }
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN disclaimerWillShow WHEN scanAndSaveUserWallet THEN router pop AND no save`() = runTest {
|
||||
mockScanCallback(callbackName = DISCLAIMER_WILL_SHOW)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify { router.pop(onComplete = any()) }
|
||||
verify(exactly = 0) { messageSender.send(any()) }
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onCancel WHEN scanAndSaveUserWallet THEN no message AND no save`() = runTest {
|
||||
mockScanCallback(callbackName = ON_CANCEL)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify(exactly = 0) { messageSender.send(any()) }
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onFailure with messageResId WHEN scanAndSaveUserWallet THEN SnackbarMessage with resource is sent`() =
|
||||
runTest {
|
||||
val tangemError = mockk<TangemError> {
|
||||
every { silent } returns false
|
||||
every { messageResId } returns R.string.common_unknown_error
|
||||
every { customMessage } returns "any"
|
||||
}
|
||||
mockScanFailure(tangemError)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onFailure without messageResId WHEN scanAndSaveUserWallet THEN SnackbarMessage with custom message`() =
|
||||
runTest {
|
||||
val tangemError = mockk<TangemError> {
|
||||
every { silent } returns false
|
||||
every { messageResId } returns null
|
||||
every { customMessage } returns "Custom error"
|
||||
}
|
||||
mockScanFailure(tangemError)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = SnackbarMessage(message = stringReference("Custom error")),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onFailure silent WHEN scanAndSaveUserWallet THEN no message is sent`() = runTest {
|
||||
val tangemError = mockk<TangemError> {
|
||||
every { silent } returns true
|
||||
every { messageResId } returns null
|
||||
every { customMessage } returns "any"
|
||||
}
|
||||
mockScanFailure(tangemError)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify(exactly = 0) { messageSender.send(any()) }
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND save success AND addressSync disabled WHEN scanAndSaveUserWallet THEN popTo Wallet`() =
|
||||
runTest {
|
||||
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(userWallet)
|
||||
coEvery { saveWalletUseCase.invoke(userWallet, false, any()) } returns Either.Right(Unit)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify { router.popTo(routeClass = AppRoute.Wallet::class, onComplete = any()) }
|
||||
verify(exactly = 0) { router.push(route = any(), onComplete = any()) }
|
||||
verify(exactly = 0) { messageSender.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND save success AND addressSync enabled WHEN scanAndSaveUserWallet THEN push Onboarding`() =
|
||||
runTest {
|
||||
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns true
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(userWallet)
|
||||
coEvery { saveWalletUseCase.invoke(userWallet, false, any()) } returns Either.Right(Unit)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
router.push(
|
||||
route = AppRoute.Onboarding(
|
||||
scanResponse = scanResponse,
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(
|
||||
userWalletId = userWalletId,
|
||||
isWalletStarted = true,
|
||||
),
|
||||
),
|
||||
onComplete = any(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 0) { router.popTo(routeClass = any(), onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND createUserWallet returns null WHEN scanAndSaveUserWallet THEN unknown error message`() =
|
||||
runTest {
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(null)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { saveWalletUseCase.invoke(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND save WalletAlreadySaved WHEN scanAndSaveUserWallet THEN DialogMessage is sent`() =
|
||||
runTest {
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(userWallet)
|
||||
coEvery { saveWalletUseCase.invoke(userWallet, false, any()) } returns Either.Left(
|
||||
SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved),
|
||||
)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(R.string.user_wallet_list_error_wallet_already_saved),
|
||||
),
|
||||
)
|
||||
}
|
||||
verify(exactly = 0) { router.popTo(routeClass = any(), onComplete = any()) }
|
||||
verify(exactly = 0) { router.push(route = any(), onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND save DataError with messageId WHEN scanAndSaveUserWallet THEN snackbar with resource`() =
|
||||
runTest {
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(userWallet)
|
||||
coEvery { saveWalletUseCase.invoke(userWallet, false, any()) } returns Either.Left(
|
||||
SaveWalletError.DataError(messageId = R.string.common_unknown_error),
|
||||
)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)),
|
||||
)
|
||||
}
|
||||
verify(exactly = 0) { router.popTo(routeClass = any(), onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onSuccess AND save DataError without messageId WHEN scanAndSaveUserWallet THEN unknown error snackbar`() =
|
||||
runTest {
|
||||
mockScanSuccess(scanResponse)
|
||||
mockBuilderReturns(userWallet)
|
||||
coEvery { saveWalletUseCase.invoke(userWallet, false, any()) } returns Either.Left(
|
||||
SaveWalletError.DataError(messageId = null),
|
||||
)
|
||||
|
||||
createSaver().scanAndSaveUserWallet(this)
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)),
|
||||
)
|
||||
}
|
||||
verify(exactly = 0) { router.popTo(routeClass = any(), onComplete = any()) }
|
||||
}
|
||||
|
||||
private fun mockScanCallback(callbackName: String) {
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = any(),
|
||||
shouldCheckIsAlreadyActivated = any(),
|
||||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
when (callbackName) {
|
||||
ON_WALLET_NOT_CREATED -> arg<suspend () -> Unit>(ON_WALLET_NOT_CREATED_INDEX).invoke()
|
||||
DISCLAIMER_WILL_SHOW -> arg<() -> Unit>(DISCLAIMER_WILL_SHOW_INDEX).invoke()
|
||||
ON_CANCEL -> arg<suspend () -> Unit>(ON_CANCEL_INDEX).invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockScanFailure(tangemError: TangemError) {
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = any(),
|
||||
shouldCheckIsAlreadyActivated = any(),
|
||||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
arg<suspend (TangemError) -> Unit>(ON_FAILURE_INDEX).invoke(tangemError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockScanSuccess(scanResponse: ScanResponse) {
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = any(),
|
||||
shouldCheckIsAlreadyActivated = any(),
|
||||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
arg<suspend (ScanResponse) -> Unit>(ON_SUCCESS_INDEX).invoke(scanResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockBuilderReturns(userWallet: UserWallet.Cold?) {
|
||||
val builder: ColdUserWalletBuilder = mockk {
|
||||
every { build() } returns userWallet
|
||||
}
|
||||
every { coldUserWalletBuilderFactory.create(scanResponse = any()) } returns builder
|
||||
}
|
||||
|
||||
private fun createSaver(): UserWalletSaver {
|
||||
return UserWalletSaver(
|
||||
scanCardProcessor = scanCardProcessor,
|
||||
saveWalletUseCase = saveWalletUseCase,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
messageSender = messageSender,
|
||||
router = router,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ON_WALLET_NOT_CREATED = "onWalletNotCreated"
|
||||
const val DISCLAIMER_WILL_SHOW = "disclaimerWillShow"
|
||||
const val ON_CANCEL = "onCancel"
|
||||
|
||||
const val ON_WALLET_NOT_CREATED_INDEX = 4
|
||||
const val DISCLAIMER_WILL_SHOW_INDEX = 5
|
||||
const val ON_CANCEL_INDEX = 6
|
||||
const val ON_FAILURE_INDEX = 7
|
||||
const val ON_SUCCESS_INDEX = 8
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.addresssync
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -117,7 +116,6 @@ internal class DefaultAddressSyncComponent(
|
|||
AddressSyncState.Loading -> AddressSyncLoading()
|
||||
is AddressSyncState.Success -> AddressSyncButtonScreen(
|
||||
state = state as AddressSyncState.Success,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onSyncClick = {
|
||||
model.onIntent(AddressSyncIntent.Sync)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
package com.tangem.features.onboarding.v2.entry.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class OnboardingEntryModelTest {
|
||||
|
||||
private val router: Router = mockk(relaxUnitFun = true)
|
||||
private val tangemSdkManager: TangemSdkManager = mockk()
|
||||
private val settingsRepository: SettingsRepository = mockk()
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true)
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxUnitFun = true)
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
private val paramsContainer: ParamsContainer = mockk()
|
||||
|
||||
private val scanResponse: ScanResponse = mockk()
|
||||
private val params: OnboardingEntryComponent.Params = mockk {
|
||||
every { scanResponse } returns this@OnboardingEntryModelTest.scanResponse
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
every { paramsContainer.require<OnboardingEntryComponent.Params>() } returns params
|
||||
every { params.mode } returns OnboardingEntryComponent.Mode.Onboarding
|
||||
every { scanResponse.productType } returns ProductType.Wallet
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns false
|
||||
coEvery { settingsRepository.shouldShowAskBiometry() } returns false
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideStartRouteByProductType")
|
||||
fun `GIVEN product type WHEN model is created THEN startRoute is of expected type`(
|
||||
productType: ProductType,
|
||||
expectedRouteClass: KClass<out OnboardingRoute>,
|
||||
) = runTest {
|
||||
every { scanResponse.productType } returns productType
|
||||
every { params.mode } returns OnboardingEntryComponent.Mode.Onboarding
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
Assertions.assertTrue(
|
||||
expectedRouteClass.isInstance(model.startRoute),
|
||||
"Expected ${expectedRouteClass.simpleName} but got ${model.startRoute::class.simpleName}",
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideWallet2ModeMappings")
|
||||
fun `GIVEN Wallet2 AND entry mode WHEN model is created THEN multi-wallet mode is mapped`(
|
||||
entryMode: OnboardingEntryComponent.Mode,
|
||||
expectedMultiWalletMode: OnboardingMultiWalletComponent.Mode,
|
||||
) = runTest {
|
||||
every { scanResponse.productType } returns ProductType.Wallet2
|
||||
every { params.mode } returns entryMode
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
val route = model.startRoute as OnboardingRoute.MultiWallet
|
||||
Assertions.assertEquals(expectedMultiWalletMode, route.mode)
|
||||
Assertions.assertEquals(true, route.withSeedPhraseFlow)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTwinsModeMappings")
|
||||
fun `GIVEN Twins AND entry mode WHEN model is created THEN twin mode is mapped`(
|
||||
entryMode: OnboardingEntryComponent.Mode,
|
||||
expectedTwinMode: OnboardingTwinComponent.Params.Mode,
|
||||
) = runTest {
|
||||
every { scanResponse.productType } returns ProductType.Twins
|
||||
every { params.mode } returns entryMode
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
val route = model.startRoute as OnboardingRoute.Twins
|
||||
Assertions.assertEquals(expectedTwinMode, route.mode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Wallet WHEN model is created THEN withSeedPhraseFlow is false`() = runTest {
|
||||
every { scanResponse.productType } returns ProductType.Wallet
|
||||
every { params.mode } returns OnboardingEntryComponent.Mode.Onboarding
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
val route = model.startRoute as OnboardingRoute.MultiWallet
|
||||
Assertions.assertEquals(false, route.withSeedPhraseFlow)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN biometry available AND should ask WHEN onManageTokensDone THEN AskBiometry replaces stack`() = runTest {
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns true
|
||||
coEvery { settingsRepository.shouldShowAskBiometry() } returns true
|
||||
|
||||
val model = createModel(this)
|
||||
val stack = model.stackNavigation.trackStack()
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(1, stack.size)
|
||||
Assertions.assertTrue(stack.first() is OnboardingRoute.AskBiometry)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN biometry not available WHEN onManageTokensDone THEN Done WalletCreated replaces stack`() = runTest {
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns false
|
||||
|
||||
val model = createModel(this)
|
||||
val stack = model.stackNavigation.trackStack()
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(1, stack.size)
|
||||
val route = stack.first()
|
||||
Assertions.assertTrue(route is OnboardingRoute.Done)
|
||||
Assertions.assertEquals(OnboardingDoneComponent.Mode.WalletCreated, (route as OnboardingRoute.Done).mode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN biometry available AND should not ask WHEN onManageTokensDone THEN Done replaces stack`() = runTest {
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns true
|
||||
coEvery { settingsRepository.shouldShowAskBiometry() } returns false
|
||||
|
||||
val model = createModel(this)
|
||||
val stack = model.stackNavigation.trackStack()
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(1, stack.size)
|
||||
Assertions.assertTrue(stack.first() is OnboardingRoute.Done)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Visa AND biometry available WHEN onManageTokensDone THEN BiometricScreenOpened analytics is sent`() =
|
||||
runTest {
|
||||
every { scanResponse.productType } returns ProductType.Visa
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns true
|
||||
coEvery { settingsRepository.shouldShowAskBiometry() } returns true
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(match<OnboardingVisaAnalyticsEvent.BiometricScreenOpened> { true })
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(match<OnboardingVisaAnalyticsEvent.SuccessScreenOpened> { true })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Visa AND biometry not available WHEN onManageTokensDone THEN SuccessScreenOpened analytics is sent`() =
|
||||
runTest {
|
||||
every { scanResponse.productType } returns ProductType.Visa
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns false
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(match<OnboardingVisaAnalyticsEvent.SuccessScreenOpened> { true })
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(match<OnboardingVisaAnalyticsEvent.BiometricScreenOpened> { true })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Visa WHEN onManageTokensDone THEN no Visa analytics sent`() = runTest {
|
||||
every { scanResponse.productType } returns ProductType.Wallet2
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns true
|
||||
coEvery { settingsRepository.shouldShowAskBiometry() } returns true
|
||||
|
||||
val model = createModel(this)
|
||||
|
||||
model.onManageTokensDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(match<OnboardingVisaAnalyticsEvent> { true })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onBack THEN CantLeaveBackupDialog is sent`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onBack()
|
||||
|
||||
verify { uiMessageSender.send(CantLeaveBackupDialog) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onboardingTwinModelCallbacks onBack THEN router pop is called`() = runTest {
|
||||
val model = createModel(this)
|
||||
|
||||
model.onboardingTwinModelCallbacks.onBack()
|
||||
|
||||
verify { router.pop(onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onboardingTwinModelCallbacks onDone THEN navigateToFinalScreenFlow runs`() = runTest {
|
||||
coEvery { tangemSdkManager.checkCanUseBiometry() } returns false
|
||||
|
||||
val model = createModel(this)
|
||||
val stack = model.stackNavigation.trackStack()
|
||||
|
||||
model.onboardingTwinModelCallbacks.onDone()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(1, stack.size)
|
||||
val route = stack.first()
|
||||
Assertions.assertTrue(route is OnboardingRoute.Done)
|
||||
Assertions.assertEquals(OnboardingDoneComponent.Mode.WalletCreated, (route as OnboardingRoute.Done).mode)
|
||||
}
|
||||
|
||||
private fun StackNavigation<OnboardingRoute>.trackStack(): List<OnboardingRoute> {
|
||||
val tracked = mutableListOf<OnboardingRoute>()
|
||||
subscribe { event ->
|
||||
val newStack = event.transformer(tracked.toList())
|
||||
tracked.clear()
|
||||
tracked.addAll(newStack)
|
||||
}
|
||||
return tracked
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope): OnboardingEntryModel {
|
||||
return OnboardingEntryModel(
|
||||
paramsContainer = paramsContainer,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
settingsRepository = settingsRepository,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
uiMessageSender = uiMessageSender,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun provideStartRouteByProductType(): List<Arguments> = listOf(
|
||||
Arguments.of(ProductType.Wallet, OnboardingRoute.MultiWallet::class),
|
||||
Arguments.of(ProductType.Wallet2, OnboardingRoute.MultiWallet::class),
|
||||
Arguments.of(ProductType.Ring, OnboardingRoute.MultiWallet::class),
|
||||
Arguments.of(ProductType.Note, OnboardingRoute.Note::class),
|
||||
Arguments.of(ProductType.Twins, OnboardingRoute.Twins::class),
|
||||
Arguments.of(ProductType.Visa, OnboardingRoute.Visa::class),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun provideWallet2ModeMappings(): List<Arguments> {
|
||||
val userWalletId = UserWalletId("011")
|
||||
return listOf(
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.Onboarding,
|
||||
OnboardingMultiWalletComponent.Mode.Onboarding,
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.AddBackupWallet1,
|
||||
OnboardingMultiWalletComponent.Mode.AddBackup,
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.ContinueFinalize,
|
||||
OnboardingMultiWalletComponent.Mode.ContinueFinalize,
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.UpgradeHotWallet(userWalletId),
|
||||
OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(userWalletId),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.AddressSync(userWalletId, isWalletStarted = true),
|
||||
OnboardingMultiWalletComponent.Mode.AddressSync(userWalletId, isWalletStarted = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun provideTwinsModeMappings(): List<Arguments> = listOf(
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.Onboarding,
|
||||
OnboardingTwinComponent.Params.Mode.CreateWallet,
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.WelcomeOnlyTwin,
|
||||
OnboardingTwinComponent.Params.Mode.WelcomeOnly,
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingEntryComponent.Mode.RecreateWalletTwin,
|
||||
OnboardingTwinComponent.Params.Mode.RecreateWallet,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,503 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase
|
||||
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state.MultiWalletFinalizeUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class MultiWalletFinalizeModelTest {
|
||||
|
||||
private val backupServiceHolder: BackupServiceHolder = mockk()
|
||||
private val backupService: BackupService = mockk()
|
||||
private val backupServiceWeakRef: WeakReference<BackupService> = WeakReference(backupService)
|
||||
private val tangemSdkManager: TangemSdkManager = mockk(relaxUnitFun = true)
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk()
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk()
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory = mockk()
|
||||
private val saveWalletUseCase: SaveWalletUseCase = mockk()
|
||||
private val getUserWalletsUseCase: GetWalletsUseCase = mockk()
|
||||
private val updateWalletUseCase: UpdateWalletUseCase = mockk()
|
||||
private val syncWalletWithRemoteUseCase: SyncWalletWithRemoteUseCase = mockk()
|
||||
private val cardRepository: CardRepository = mockk()
|
||||
private val onboardingRepository: OnboardingRepository = mockk(relaxUnitFun = true)
|
||||
private val walletsRepository: WalletsRepository = mockk()
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxUnitFun = true)
|
||||
private val backupValidator: BackupValidator = mockk()
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true)
|
||||
private val paramsContainer: ParamsContainer = mockk()
|
||||
|
||||
private val scanResponse: ScanResponse = mockk()
|
||||
|
||||
private val multiWalletStateFlow = MutableStateFlow(
|
||||
OnboardingMultiWalletState(
|
||||
currentStep = OnboardingMultiWalletState.Step.Finalize,
|
||||
accessCode = null,
|
||||
isThreeCards = true,
|
||||
currentScanResponse = scanResponse,
|
||||
startFromFinalize = null,
|
||||
resultUserWallet = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val parentParams: OnboardingMultiWalletComponent.Params = mockk {
|
||||
every { mode } returns OnboardingMultiWalletComponent.Mode.Onboarding
|
||||
every { scanResponse } returns this@MultiWalletFinalizeModelTest.scanResponse
|
||||
}
|
||||
|
||||
private val params: MultiWalletChildParams = mockk {
|
||||
every { multiWalletState } returns multiWalletStateFlow
|
||||
every { parentParams } returns this@MultiWalletFinalizeModelTest.parentParams
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
every { paramsContainer.require<MultiWalletChildParams>() } returns params
|
||||
every { backupServiceHolder.backupService } returns backupServiceWeakRef
|
||||
every { backupService.primaryCardId } returns "primary-id-aaaa"
|
||||
every { backupService.primaryCardBatchId } returns NON_RING_BATCH_ID
|
||||
every { backupService.backupCardIds } returns listOf("backup-1-bbbb", "backup-2-cccc")
|
||||
every { backupService.backupCardsBatchIds } returns listOf(NON_RING_BATCH_ID, NON_RING_BATCH_ID)
|
||||
every { backupService.currentState } returns BackupService.State.FinalizingPrimaryCard
|
||||
coEvery { onboardingRepository.saveUnfinishedFinalizeOnboarding(any()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN init AND startFromFinalize is null THEN no events emitted`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(startFromFinalize = null)
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect { events.add(it) } }
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(emptyList<MultiWalletFinalizeComponent.Event>(), events)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN init AND startFromFinalize is ScanBackupFirstCard THEN OneBackupCardAdded emitted`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect { events.add(it) } }
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(
|
||||
listOf(MultiWalletFinalizeComponent.Event.OneBackupCardAdded),
|
||||
events,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN init AND startFromFinalize is ScanBackupSecondCard THEN both events emitted in order`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupSecondCard,
|
||||
)
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect { events.add(it) } }
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(
|
||||
listOf(
|
||||
MultiWalletFinalizeComponent.Event.OneBackupCardAdded,
|
||||
MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded,
|
||||
),
|
||||
events,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backupService is null WHEN model is created THEN initial state is default`() = runTest {
|
||||
every { backupServiceHolder.backupService } returns WeakReference(null)
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.Primary, state.step)
|
||||
Assertions.assertEquals(true, state.scanPrimary)
|
||||
Assertions.assertEquals("", state.cardNumber)
|
||||
Assertions.assertEquals(false, state.isRing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN startFromFinalize null AND non-Ring primary WHEN model is created THEN state is Primary non-Ring`() =
|
||||
runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(startFromFinalize = null)
|
||||
every { backupService.primaryCardBatchId } returns NON_RING_BATCH_ID
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.Primary, state.step)
|
||||
Assertions.assertEquals(true, state.scanPrimary)
|
||||
Assertions.assertEquals(false, state.isRing)
|
||||
Assertions.assertEquals("primary-id-aaaa".lastMaskedExpected(), state.cardNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN startFromFinalize ScanPrimaryCard AND Ring primary WHEN model is created THEN state is Primary Ring`() =
|
||||
runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanPrimaryCard,
|
||||
)
|
||||
every { backupService.primaryCardBatchId } returns RING_BATCH_ID_AC17
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.Primary, state.step)
|
||||
Assertions.assertEquals(true, state.scanPrimary)
|
||||
Assertions.assertEquals(true, state.isRing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN startFromFinalize ScanBackupFirstCard WHEN model is created THEN state is BackupDevice1`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice1, state.step)
|
||||
Assertions.assertEquals(false, state.scanPrimary)
|
||||
Assertions.assertEquals("backup-1-bbbb".lastMaskedExpected(), state.cardNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN startFromFinalize ScanBackupSecondCard WHEN model is created THEN state is BackupDevice2`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupSecondCard,
|
||||
)
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice2, state.step)
|
||||
Assertions.assertEquals(false, state.scanPrimary)
|
||||
Assertions.assertEquals("backup-2-cccc".lastMaskedExpected(), state.cardNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN scanPrimary true WHEN onBack THEN onBackFlow emits Unit`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(startFromFinalize = null)
|
||||
|
||||
val model = createModel(this)
|
||||
val received = mutableListOf<Unit>()
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onBackFlow.collect { received.add(it) } }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBack()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(listOf(Unit), received)
|
||||
verify(exactly = 0) { uiMessageSender.send(CantLeaveBackupDialog) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN scanPrimary false WHEN onBack THEN CantLeaveBackupDialog is sent`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBack()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { uiMessageSender.send(CantLeaveBackupDialog) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN primary batchId is null WHEN onScanClick THEN proceedBackup is not called`() = runTest {
|
||||
every { backupService.primaryCardBatchId } returns null
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { backupService.proceedBackup(iconScanRes = any(), callback = any()) }
|
||||
verify(exactly = 0) { tangemSdkManager.changeProductType(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Ring primary AND success WHEN onScanClick THEN state moves to BackupDevice1`() = runTest {
|
||||
every { backupService.primaryCardBatchId } returns NON_RING_BATCH_ID
|
||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = null, callback = capture(callbackSlot))
|
||||
} just Runs
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect { events.add(it) } }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { tangemSdkManager.changeProductType(false) }
|
||||
verify { backupService.proceedBackup(iconScanRes = null, callback = any()) }
|
||||
|
||||
callbackSlot.captured.invoke(CompletionResult.Success(mockk()))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { tangemSdkManager.clearProductType() }
|
||||
coVerify { onboardingRepository.saveUnfinishedFinalizeOnboarding(scanResponse = scanResponse) }
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice1, state.step)
|
||||
Assertions.assertEquals(false, state.scanPrimary)
|
||||
Assertions.assertEquals("backup-1-bbbb".lastMaskedExpected(), state.cardNumber)
|
||||
Assertions.assertEquals(false, state.isRing)
|
||||
Assertions.assertEquals(
|
||||
listOf(MultiWalletFinalizeComponent.Event.OneBackupCardAdded),
|
||||
events,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Ring primary WHEN onScanClick THEN changeProductType is true and ring icon is used`() = runTest {
|
||||
every { backupService.primaryCardBatchId } returns RING_BATCH_ID_AC17
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = any(), callback = any())
|
||||
} just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { tangemSdkManager.changeProductType(true) }
|
||||
verify {
|
||||
backupService.proceedBackup(
|
||||
iconScanRes = com.tangem.features.onboarding.v2.impl.R.drawable.img_hand_scan_ring,
|
||||
callback = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN primary AND failure WHEN onScanClick THEN state is unchanged AND no event emitted`() = runTest {
|
||||
every { backupService.primaryCardBatchId } returns NON_RING_BATCH_ID
|
||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = null, callback = capture(callbackSlot))
|
||||
} just Runs
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect { events.add(it) } }
|
||||
advanceUntilIdle()
|
||||
val stateBefore = model.uiState.value
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
callbackSlot.captured.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { tangemSdkManager.clearProductType() }
|
||||
coVerify(exactly = 0) { onboardingRepository.saveUnfinishedFinalizeOnboarding(any()) }
|
||||
Assertions.assertEquals(stateBefore.step, model.uiState.value.step)
|
||||
Assertions.assertEquals(stateBefore.scanPrimary, model.uiState.value.scanPrimary)
|
||||
Assertions.assertEquals(emptyList<MultiWalletFinalizeComponent.Event>(), events)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN BackupDevice1 AND batchId null WHEN onScanClick THEN proceedBackup is not called`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
every { backupService.backupCardsBatchIds } returns emptyList()
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { backupService.proceedBackup(iconScanRes = any(), callback = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN BackupDevice1 AND failure WalletAlreadyCreated WHEN onScanClick THEN dialog is set`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = null, callback = capture(callbackSlot))
|
||||
} just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
callbackSlot.captured.invoke(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated()))
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertNotNull(model.uiState.value.dialog)
|
||||
verify { tangemSdkManager.clearProductType() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN BackupDevice1 AND failure other error WHEN onScanClick THEN no dialog is set`() = runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = null, callback = capture(callbackSlot))
|
||||
} just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) { model.onEvent.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
callbackSlot.captured.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertNull(model.uiState.value.dialog)
|
||||
verify { tangemSdkManager.clearProductType() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN BackupDevice1 AND success AND not Finished WHEN onScanClick THEN state moves to BackupDevice2`() =
|
||||
runTest {
|
||||
multiWalletStateFlow.value = multiWalletStateFlow.value.copy(
|
||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||
)
|
||||
every { backupService.currentState } returns BackupService.State.FinalizingBackupCard(index = 1)
|
||||
|
||||
mockkConstructor(BackupValidator::class)
|
||||
every { anyConstructed<BackupValidator>().isValidBackupStatus(any()) } returns true
|
||||
|
||||
val card: Card = mockk(relaxed = true)
|
||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||
every {
|
||||
backupService.proceedBackup(iconScanRes = null, callback = capture(callbackSlot))
|
||||
} just Runs
|
||||
|
||||
val events = mutableListOf<MultiWalletFinalizeComponent.Event>()
|
||||
val model = createModel(this)
|
||||
backgroundScope.launch(context = Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) {
|
||||
model.onEvent.collect { events.add(it) }
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
model.uiState.value.onScanClick.invoke()
|
||||
advanceUntilIdle()
|
||||
callbackSlot.captured.invoke(CompletionResult.Success(card))
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = model.uiState.value
|
||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice2, state.step)
|
||||
Assertions.assertEquals("backup-2-cccc".lastMaskedExpected(), state.cardNumber)
|
||||
Assertions.assertTrue(events.contains(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded))
|
||||
|
||||
unmockkConstructor(BackupValidator::class)
|
||||
}
|
||||
|
||||
private fun String.lastMaskedExpected(): String {
|
||||
val space = ' '
|
||||
val last4 = takeLast(4)
|
||||
return "$space*$space*$space*$space$last4"
|
||||
}
|
||||
|
||||
private fun createModel(testScope: TestScope): MultiWalletFinalizeModel {
|
||||
return MultiWalletFinalizeModel(
|
||||
paramsContainer = paramsContainer,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
backupServiceHolder = backupServiceHolder,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
|
||||
saveWalletUseCase = saveWalletUseCase,
|
||||
getUserWalletsUseCase = getUserWalletsUseCase,
|
||||
updateWalletUseCase = updateWalletUseCase,
|
||||
syncWalletWithRemoteUseCase = syncWalletWithRemoteUseCase,
|
||||
cardRepository = cardRepository,
|
||||
onboardingRepository = onboardingRepository,
|
||||
walletsRepository = walletsRepository,
|
||||
uiMessageSender = uiMessageSender,
|
||||
backupValidator = backupValidator,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val NON_RING_BATCH_ID = "AC02"
|
||||
private const val RING_BATCH_ID_AC17 = "AC17"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,454 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.model
|
||||
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.artwork.ArtworkUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.ArtworkModel
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.features.onboarding.v2.TitleProvider
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.title.OnboardingTitle
|
||||
import com.tangem.operations.attestation.ArtworkSize
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class OnboardingMultiWalletModelTest {
|
||||
|
||||
private val analyticsHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true)
|
||||
private val router: Router = mockk(relaxUnitFun = true)
|
||||
private val backupServiceHolder: BackupServiceHolder = mockk()
|
||||
private val backupServiceWeakRef: WeakReference<BackupService> = WeakReference(null)
|
||||
private val onboardingRepository: OnboardingRepository = mockk(relaxUnitFun = true)
|
||||
private val getCardImageUseCase: GetCardImageUseCase = mockk()
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxUnitFun = true)
|
||||
private val artworkUMConverter: ArtworkUMConverter = mockk()
|
||||
private val paramsContainer: ParamsContainer = mockk()
|
||||
private val titleProvider: TitleProvider = mockk(relaxUnitFun = true)
|
||||
|
||||
private val card1Id = "card-id-1"
|
||||
private val card1PublicKey = byteArrayOf(1, 2, 3)
|
||||
private val card1ManufacturerName = "Tangem"
|
||||
private val card1Manufacturer = CardDTO.Manufacturer(
|
||||
name = card1ManufacturerName,
|
||||
manufactureDate = Date(0),
|
||||
signature = null,
|
||||
)
|
||||
private val card1FirmwareVersionDto = CardDTO.FirmwareVersion(
|
||||
major = 6,
|
||||
minor = 33,
|
||||
patch = 0,
|
||||
type = SdkFirmwareVersion.FirmwareType.Release,
|
||||
)
|
||||
private val card1SdkFirmwareVersion = SdkFirmwareVersion(major = 6, minor = 33)
|
||||
private val cardDto: CardDTO = mockk()
|
||||
private val scanResponse: ScanResponse = mockk()
|
||||
private val artwork1Model = ArtworkModel(verifiedArtwork = null, defaultUrl = "default-url-1")
|
||||
private val artwork1Um = ArtworkUM(verifiedArtwork = null, defaultUrl = "default-url-1")
|
||||
|
||||
private val params: OnboardingMultiWalletComponent.Params = mockk {
|
||||
every { titleProvider } returns this@OnboardingMultiWalletModelTest.titleProvider
|
||||
every { scanResponse } returns this@OnboardingMultiWalletModelTest.scanResponse
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
every { paramsContainer.require<OnboardingMultiWalletComponent.Params>() } returns params
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.Onboarding
|
||||
|
||||
every { cardDto.cardId } returns card1Id
|
||||
every { cardDto.cardPublicKey } returns card1PublicKey
|
||||
every { cardDto.manufacturer } returns card1Manufacturer
|
||||
every { cardDto.firmwareVersion } returns card1FirmwareVersionDto
|
||||
every { cardDto.wallets } returns emptyList()
|
||||
every { cardDto.backupStatus } returns null
|
||||
|
||||
every { scanResponse.card } returns cardDto
|
||||
every { scanResponse.productType } returns ProductType.Wallet
|
||||
every { scanResponse.primaryCard } returns null
|
||||
|
||||
every { backupServiceHolder.backupService } returns backupServiceWeakRef
|
||||
|
||||
coEvery {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card1Id,
|
||||
cardPublicKey = card1PublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card1ManufacturerName,
|
||||
firmwareVersion = card1SdkFirmwareVersion,
|
||||
)
|
||||
} returns artwork1Model
|
||||
every { artworkUMConverter.convert(artwork1Model) } returns artwork1Um
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN model is created THEN OnboardingEvent Started is sent`() = runTest {
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsHandler.send(match<OnboardingEvent.Started> { true }) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN UpgradeHotWallet mode WHEN model is created THEN title is common_tangem`() = runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
|
||||
userWalletId = UserWalletId("011"),
|
||||
)
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.common_tangem)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ContinueFinalize mode WHEN model is created THEN title is finalize_backup`() = runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.ContinueFinalize
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_button_finalize_backup)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN AddressSync mode WHEN model is created THEN title is biometrics`() = runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(
|
||||
userWalletId = UserWalletId("011"),
|
||||
isWalletStarted = false,
|
||||
)
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_navbar_title_biometrics)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets present AND NoBackup AND no primary card WHEN created THEN title is creating_backup`() = runTest {
|
||||
every { cardDto.wallets } returns listOf(mockk())
|
||||
every { cardDto.backupStatus } returns CardDTO.BackupStatus.NoBackup
|
||||
every { scanResponse.primaryCard } returns null
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_navbar_title_creating_backup)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets present AND NoBackup AND Wallet productType WHEN created THEN title is getting_started`() =
|
||||
runTest {
|
||||
every { cardDto.wallets } returns listOf(mockk())
|
||||
every { cardDto.backupStatus } returns CardDTO.BackupStatus.NoBackup
|
||||
every { scanResponse.primaryCard } returns mockk()
|
||||
every { scanResponse.productType } returns ProductType.Wallet
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_getting_started)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets present AND NoBackup AND non-Wallet productType WHEN created THEN title is creating_backup`() =
|
||||
runTest {
|
||||
every { cardDto.wallets } returns listOf(mockk())
|
||||
every { cardDto.backupStatus } returns CardDTO.BackupStatus.NoBackup
|
||||
every { scanResponse.primaryCard } returns mockk()
|
||||
every { scanResponse.productType } returns ProductType.Wallet2
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_navbar_title_creating_backup)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets present AND active backup WHEN created THEN title is finalize_backup`() = runTest {
|
||||
every { cardDto.wallets } returns listOf(mockk())
|
||||
every { cardDto.backupStatus } returns CardDTO.BackupStatus.Active(cardCount = 2)
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_button_finalize_backup)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallets WHEN model is created THEN title is create_wallet_header`() = runTest {
|
||||
every { cardDto.wallets } returns emptyList()
|
||||
|
||||
createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
titleProvider.changeTitle(
|
||||
title = OnboardingTitle(text = resourceReference(R.string.onboarding_create_wallet_header)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN model is created THEN loadCardArtwork updates artwork1 in uiState`() = runTest {
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card1Id,
|
||||
cardPublicKey = card1PublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card1ManufacturerName,
|
||||
firmwareVersion = card1SdkFirmwareVersion,
|
||||
)
|
||||
}
|
||||
verify { artworkUMConverter.convert(artwork1Model) }
|
||||
Assertions.assertEquals(artwork1Um, model.uiState.value.artwork1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backups emit card2 WHEN subscribeToBackups THEN artwork2 is loaded and updated`() = runTest {
|
||||
val card2Info = card2BackupInfo()
|
||||
val artwork2Model = ArtworkModel(verifiedArtwork = null, defaultUrl = "default-url-2")
|
||||
val artwork2Um = ArtworkUM(verifiedArtwork = null, defaultUrl = "default-url-2")
|
||||
coEvery {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card2Info.cardId,
|
||||
cardPublicKey = card2Info.cardPublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card2Info.manufacturer.name,
|
||||
firmwareVersion = card2Info.firmwareVersion,
|
||||
)
|
||||
} returns artwork2Model
|
||||
every { artworkUMConverter.convert(artwork2Model) } returns artwork2Um
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.backups.value = MultiWalletChildParams.Backup(card2 = card2Info)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card2Info.cardId,
|
||||
cardPublicKey = card2Info.cardPublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card2Info.manufacturer.name,
|
||||
firmwareVersion = card2Info.firmwareVersion,
|
||||
)
|
||||
}
|
||||
Assertions.assertEquals(artwork2Um, model.uiState.value.artwork2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backups emit card3 after card2 WHEN subscribeToBackups THEN artwork3 is loaded and updated`() = runTest {
|
||||
val card2Info = card2BackupInfo()
|
||||
val card3Info = card3BackupInfo()
|
||||
val artwork2Model = ArtworkModel(verifiedArtwork = null, defaultUrl = "default-url-2")
|
||||
val artwork2Um = ArtworkUM(verifiedArtwork = null, defaultUrl = "default-url-2")
|
||||
val artwork3Model = ArtworkModel(verifiedArtwork = null, defaultUrl = "default-url-3")
|
||||
val artwork3Um = ArtworkUM(verifiedArtwork = null, defaultUrl = "default-url-3")
|
||||
coEvery {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card2Info.cardId,
|
||||
cardPublicKey = card2Info.cardPublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card2Info.manufacturer.name,
|
||||
firmwareVersion = card2Info.firmwareVersion,
|
||||
)
|
||||
} returns artwork2Model
|
||||
every { artworkUMConverter.convert(artwork2Model) } returns artwork2Um
|
||||
coEvery {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card3Info.cardId,
|
||||
cardPublicKey = card3Info.cardPublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card3Info.manufacturer.name,
|
||||
firmwareVersion = card3Info.firmwareVersion,
|
||||
)
|
||||
} returns artwork3Model
|
||||
every { artworkUMConverter.convert(artwork3Model) } returns artwork3Um
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.backups.value = MultiWalletChildParams.Backup(card2 = card2Info)
|
||||
advanceUntilIdle()
|
||||
model.backups.value = MultiWalletChildParams.Backup(card2 = card2Info, card3 = card3Info)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
getCardImageUseCase.invoke(
|
||||
cardId = card3Info.cardId,
|
||||
cardPublicKey = card3Info.cardPublicKey,
|
||||
size = ArtworkSize.LARGE,
|
||||
manufacturerName = card3Info.manufacturer.name,
|
||||
firmwareVersion = card3Info.firmwareVersion,
|
||||
)
|
||||
}
|
||||
Assertions.assertEquals(artwork3Um, model.uiState.value.artwork3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-AddressSync mode WHEN onBack confirmed THEN router pop is called`() = runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.Onboarding
|
||||
coEvery { onboardingRepository.clearUnfinishedFinalizeOnboarding() } just Runs
|
||||
val dialogSlot = slot<DialogMessage>()
|
||||
every { uiMessageSender.send(capture(dialogSlot)) } just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBack()
|
||||
dialogSlot.captured.firstAction.onClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { onboardingRepository.clearUnfinishedFinalizeOnboarding() }
|
||||
verify { router.pop(onComplete = any()) }
|
||||
verify(exactly = 0) { router.popTo(route = any(), onComplete = any()) }
|
||||
verify(exactly = 0) { router.replaceAll(routes = anyVararg(), onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN AddressSync mode AND wallet started WHEN onBack confirmed THEN popTo Wallet is called`() = runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(
|
||||
userWalletId = UserWalletId("011"),
|
||||
isWalletStarted = true,
|
||||
)
|
||||
coEvery { onboardingRepository.clearUnfinishedFinalizeOnboarding() } just Runs
|
||||
val dialogSlot = slot<DialogMessage>()
|
||||
every { uiMessageSender.send(capture(dialogSlot)) } just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBack()
|
||||
dialogSlot.captured.firstAction.onClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { onboardingRepository.clearUnfinishedFinalizeOnboarding() }
|
||||
verify { router.popTo(route = AppRoute.Wallet, onComplete = any()) }
|
||||
verify(exactly = 0) { router.pop(onComplete = any()) }
|
||||
verify(exactly = 0) { router.replaceAll(routes = anyVararg(), onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN AddressSync mode AND wallet not started WHEN onBack confirmed THEN replaceAll Wallet is called`() =
|
||||
runTest {
|
||||
every { params.mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(
|
||||
userWalletId = UserWalletId("011"),
|
||||
isWalletStarted = false,
|
||||
)
|
||||
coEvery { onboardingRepository.clearUnfinishedFinalizeOnboarding() } just Runs
|
||||
val dialogSlot = slot<DialogMessage>()
|
||||
every { uiMessageSender.send(capture(dialogSlot)) } just Runs
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBack()
|
||||
dialogSlot.captured.firstAction.onClick.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { onboardingRepository.clearUnfinishedFinalizeOnboarding() }
|
||||
verify { router.replaceAll(routes = arrayOf(AppRoute.Wallet), onComplete = any()) }
|
||||
verify(exactly = 0) { router.pop(onComplete = any()) }
|
||||
verify(exactly = 0) { router.popTo(route = any(), onComplete = any()) }
|
||||
}
|
||||
|
||||
private fun card2BackupInfo() = MultiWalletChildParams.Backup.BackupCardInfo(
|
||||
cardId = "card-id-2",
|
||||
cardPublicKey = byteArrayOf(4, 5, 6),
|
||||
manufacturer = Card.Manufacturer(name = "Tangem2", manufactureDate = Date(0), signature = null),
|
||||
firmwareVersion = SdkFirmwareVersion(major = 6, minor = 34),
|
||||
)
|
||||
|
||||
private fun card3BackupInfo() = MultiWalletChildParams.Backup.BackupCardInfo(
|
||||
cardId = "card-id-3",
|
||||
cardPublicKey = byteArrayOf(7, 8, 9),
|
||||
manufacturer = Card.Manufacturer(name = "Tangem3", manufactureDate = Date(0), signature = null),
|
||||
firmwareVersion = SdkFirmwareVersion(major = 6, minor = 35),
|
||||
)
|
||||
|
||||
private fun createModel(testScope: TestScope): OnboardingMultiWalletModel {
|
||||
return OnboardingMultiWalletModel(
|
||||
paramsContainer = paramsContainer,
|
||||
analyticsHandler = analyticsHandler,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
backupServiceHolder = backupServiceHolder,
|
||||
onboardingRepository = onboardingRepository,
|
||||
getCardImageUseCase = getCardImageUseCase,
|
||||
uiMessageSender = uiMessageSender,
|
||||
artworkUMConverter = artworkUMConverter,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.model
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
internal class UtilsTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideScreenTitleByStep")
|
||||
fun `GIVEN step WHEN screenTitleByStep THEN expected text reference is returned`(
|
||||
step: OnboardingMultiWalletState.Step,
|
||||
expected: TextReference,
|
||||
) {
|
||||
val actual = screenTitleByStep(step)
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun provideScreenTitleByStep(): List<Arguments> = listOf(
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.UpgradeWallet,
|
||||
resourceReference(R.string.common_tangem),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.CreateWallet,
|
||||
resourceReference(R.string.onboarding_create_wallet_header),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.SeedPhrase,
|
||||
resourceReference(R.string.onboarding_create_wallet_header),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.ChooseBackupOption,
|
||||
resourceReference(R.string.onboarding_getting_started),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.ScanPrimary,
|
||||
resourceReference(R.string.onboarding_navbar_title_creating_backup),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.AddBackupDevice,
|
||||
resourceReference(R.string.onboarding_navbar_title_creating_backup),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.AddressSync,
|
||||
resourceReference(R.string.onboarding_navbar_title_biometrics),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.Finalize,
|
||||
resourceReference(R.string.onboarding_button_finalize_backup),
|
||||
),
|
||||
Arguments.of(
|
||||
OnboardingMultiWalletState.Step.Done,
|
||||
resourceReference(R.string.common_done),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue