Updated on 2026-08-14
This commit is contained in:
parent
935a967deb
commit
bf50e374e1
18 changed files with 617 additions and 64 deletions
|
|
@ -395,7 +395,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
componentScope.launch(dispatchers.main) {
|
||||
backupServiceHolder.backupService.get()?.discardSavedBackup()
|
||||
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
|
||||
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
|
||||
cardRepository.finishCardActivation(cardId = unfinishedBackup.card.cardId, hasBackupError = true)
|
||||
onboardingRepository.clearUnfinishedFinalizeOnboarding()
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ data class UsedCardInfo(
|
|||
val isActivationStarted: Boolean = false,
|
||||
@Json(name = "isActivationFinished")
|
||||
val isActivationFinished: Boolean = false,
|
||||
@Json(name = "hasBackupError")
|
||||
val hasBackupError: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package com.tangem.datasource.local.card
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
* Tests Moshi serialization/deserialization of [UsedCardInfo].
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class UsedCardInfoSerializationTest {
|
||||
|
||||
private val adapter = Moshi.Builder().build().adapter(UsedCardInfo::class.java)
|
||||
|
||||
@Test
|
||||
fun `GIVEN full model WHEN toJson THEN all fields serialized in declaration order`() {
|
||||
// Arrange
|
||||
val model = UsedCardInfo(
|
||||
cardId = "card-1",
|
||||
isScanned = true,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = false,
|
||||
hasBackupError = true,
|
||||
)
|
||||
|
||||
// Act
|
||||
val json = adapter.toJson(model)
|
||||
|
||||
// Assert
|
||||
assertThat(json).isEqualTo(
|
||||
"""{"cardId":"card-1","isScanned":true,"isActivationStarted":true,""" +
|
||||
""""isActivationFinished":false,"hasBackupError":true}""",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN full json WHEN fromJson THEN model fully populated`() {
|
||||
// Arrange
|
||||
val json = """{"cardId":"card-2","isScanned":false,"isActivationStarted":true,""" +
|
||||
""""isActivationFinished":true,"hasBackupError":false}"""
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
UsedCardInfo(
|
||||
cardId = "card-2",
|
||||
isScanned = false,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN json with only cardId WHEN fromJson THEN boolean fields fall back to defaults`() {
|
||||
// Arrange
|
||||
val json = """{"cardId":"card-3"}"""
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(UsedCardInfo(cardId = "card-3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN json without cardId WHEN fromJson THEN throws`() {
|
||||
// Arrange
|
||||
val json = """{"isScanned":true}"""
|
||||
|
||||
// Act
|
||||
val error = runCatching { adapter.fromJson(json) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(JsonDataException::class.java)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun roundTrip(model: UsedCardInfo) {
|
||||
// Act
|
||||
val restored = adapter.fromJson(adapter.toJson(model))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(model)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
UsedCardInfo(cardId = "default-only"),
|
||||
UsedCardInfo(
|
||||
cardId = "all-true",
|
||||
isScanned = true,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = true,
|
||||
),
|
||||
UsedCardInfo(cardId = "activation-in-progress", isScanned = true, isActivationStarted = true),
|
||||
UsedCardInfo(cardId = "backup-error", hasBackupError = true),
|
||||
)
|
||||
}
|
||||
|
|
@ -30,4 +30,8 @@ dependencies {
|
|||
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// end
|
||||
}
|
||||
|
|
@ -32,33 +32,9 @@ internal class DefaultCardRepository(
|
|||
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
|
||||
}
|
||||
|
||||
override suspend fun finishCardActivation(cardId: String) {
|
||||
override suspend fun finishCardActivation(cardId: String, hasBackupError: Boolean) {
|
||||
appPreferencesStore.editUsedCards(cardId) {
|
||||
it.copy(isActivationStarted = true, isActivationFinished = true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun finishCardsActivation(cardIds: List<String>) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val usedCards = mutablePreferences.getUsedCards()
|
||||
|
||||
val newCards = cardIds.mapNotNull { newCardId ->
|
||||
if (usedCards.none { it.cardId == newCardId }) {
|
||||
createDefaultUsedCardInfo(cardId = newCardId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val updatedUsedCards = (usedCards + newCards).map { card ->
|
||||
if (cardIds.contains(card.cardId)) {
|
||||
card.copy(isActivationStarted = true, isActivationFinished = true)
|
||||
} else {
|
||||
card
|
||||
}
|
||||
}
|
||||
|
||||
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
|
||||
it.copy(isActivationStarted = true, isActivationFinished = true, hasBackupError = hasBackupError)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +52,10 @@ internal class DefaultCardRepository(
|
|||
return card.isActivationStarted && !card.isActivationFinished
|
||||
}
|
||||
|
||||
override suspend fun hasBackupError(cardId: String): Boolean {
|
||||
return getUsedCardSync(cardId)?.hasBackupError == true
|
||||
}
|
||||
|
||||
override suspend fun isTangemTOSAccepted(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
package com.tangem.data.card
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.card.UsedCardInfo
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectList
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
* Tests for [DefaultCardRepository].
|
||||
*
|
||||
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] and a real [Moshi]
|
||||
* instance, so the JSON round-trip through preferences is exercised end-to-end.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultCardRepositoryTest {
|
||||
|
||||
// Only the in-memory store's content is mutable, so it is the single thing reset per test.
|
||||
private val dataStore = MockStateDataStore(default = emptyPreferences())
|
||||
private val appPreferencesStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = dataStore,
|
||||
)
|
||||
private val repository = DefaultCardRepository(appPreferencesStore)
|
||||
|
||||
@BeforeEach
|
||||
fun resetStore() {
|
||||
runBlocking { dataStore.updateData { emptyPreferences() } }
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class WasCardScanned {
|
||||
|
||||
@Test
|
||||
fun `GIVEN card present WHEN wasCardScanned THEN emits true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID))
|
||||
|
||||
// Act
|
||||
val result = repository.wasCardScanned(CARD_ID).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN wasCardScanned THEN emits false`() = runTest {
|
||||
// Act
|
||||
val result = repository.wasCardScanned(CARD_ID).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class SetCardWasScanned {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN setCardWasScanned THEN creates entry with isScanned true`() = runTest {
|
||||
// Act
|
||||
repository.setCardWasScanned(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isScanned = true))
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class StartCardActivation {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN startCardActivation THEN creates entry with isActivationStarted true`() = runTest {
|
||||
// Act
|
||||
repository.startCardActivation(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN other cards present WHEN editing one card THEN others are preserved`() = runTest {
|
||||
// Arrange
|
||||
val other = UsedCardInfo(cardId = OTHER_CARD_ID, isScanned = true)
|
||||
seedCards(other)
|
||||
|
||||
// Act
|
||||
repository.startCardActivation(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
other,
|
||||
UsedCardInfo(cardId = CARD_ID, isActivationStarted = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FinishCardActivation {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN finishCardActivation with backup error THEN entry marked finished with error`() =
|
||||
runTest {
|
||||
// Act
|
||||
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = true)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
UsedCardInfo(
|
||||
cardId = CARD_ID,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN finishCardActivation without backup error THEN entry marked finished without error`() =
|
||||
runTest {
|
||||
// Act
|
||||
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = false)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
UsedCardInfo(
|
||||
cardId = CARD_ID,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationStarted {
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation started WHEN isActivationStarted THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isActivationStarted(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN isActivationStarted THEN false`() = runTest {
|
||||
assertThat(repository.isActivationStarted(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationFinished {
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation finished WHEN isActivationFinished THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationFinished = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isActivationFinished(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN isActivationFinished THEN false`() = runTest {
|
||||
assertThat(repository.isActivationFinished(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationInProgress {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isActivationInProgress(model: ActivationInProgressModel) = runTest {
|
||||
// Arrange
|
||||
model.stored?.let { seedCards(it) }
|
||||
|
||||
// Act
|
||||
val result = repository.isActivationInProgress(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ActivationInProgressModel(stored = null, expected = false),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = false),
|
||||
expected = false,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = false),
|
||||
expected = true,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = true),
|
||||
expected = false,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class HasBackupError {
|
||||
|
||||
@Test
|
||||
fun `GIVEN backup error WHEN hasBackupError THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.hasBackupError(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no backup error WHEN hasBackupError THEN false`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = false))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.hasBackupError(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class TangemTos {
|
||||
|
||||
@Test
|
||||
fun `GIVEN nothing stored WHEN isTangemTOSAccepted THEN false by default`() = runTest {
|
||||
assertThat(repository.isTangemTOSAccepted()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN TOS accepted WHEN isTangemTOSAccepted THEN true`() = runTest {
|
||||
// Arrange
|
||||
repository.acceptTangemTOS()
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isTangemTOSAccepted()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedCards(vararg cards: UsedCardInfo) {
|
||||
appPreferencesStore.storeObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = cards.toList())
|
||||
}
|
||||
|
||||
private suspend fun storedCards(): List<UsedCardInfo> {
|
||||
return appPreferencesStore.getObjectListSync(key = PreferencesKeys.USED_CARDS_INFO_KEY)
|
||||
}
|
||||
|
||||
internal data class ActivationInProgressModel(val stored: UsedCardInfo?, val expected: Boolean)
|
||||
|
||||
private companion object {
|
||||
const val CARD_ID = "card-1"
|
||||
const val OTHER_CARD_ID = "card-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,7 @@ interface CardRepository {
|
|||
|
||||
suspend fun startCardActivation(cardId: String)
|
||||
|
||||
suspend fun finishCardActivation(cardId: String)
|
||||
|
||||
suspend fun finishCardsActivation(cardIds: List<String>)
|
||||
suspend fun finishCardActivation(cardId: String, hasBackupError: Boolean = false)
|
||||
|
||||
@Throws
|
||||
suspend fun isActivationStarted(cardId: String): Boolean
|
||||
|
|
@ -23,6 +21,9 @@ interface CardRepository {
|
|||
@Throws
|
||||
suspend fun isActivationInProgress(cardId: String): Boolean
|
||||
|
||||
@Throws
|
||||
suspend fun hasBackupError(cardId: String): Boolean
|
||||
|
||||
@Throws
|
||||
suspend fun isTangemTOSAccepted(): Boolean
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ dependencies {
|
|||
// end
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
// end
|
||||
}
|
||||
|
|
@ -6,11 +6,9 @@
|
|||
<ID>BooleanPropertyNaming:HotWalletPasswordRequester.kt$HotWalletPasswordRequester.AttemptRequest$val authMode: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:SaveWalletUseCase.kt$SaveWalletUseCase$val newUserWallet = userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }</ID>
|
||||
<ID>MaxChainedCallsOnSameLine:UserWalletExtensions.kt$wallets.orEmpty().first { it.curve == primaryCurve }.derivedKeys.keys.any { it == dp }</ID>
|
||||
<ID>MultilineLambdaItParameter:ColdUserWalletBuilder.kt$ColdUserWalletBuilder${ UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:HotUserWalletBuilder.kt$HotUserWalletBuilder${ MobileWallet( publicKey = it.seedKey.publicKey, chainCode = it.seedKey.chainCode, curve = it.curve, derivedKeys = it.publicKeys, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:HotUserWalletBuilder.kt$HotUserWalletBuilder${ val derivationPath = it.derivationPath(DerivationStyle.V3) ?: return@mapNotNull null if (it == Blockchain.Cardano) { val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath) listOf(derivationPath, extendedDerivationPath) } else { listOf(derivationPath) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:UpdateWalletUseCase.kt$UpdateWalletUseCase${ when (it) { is SaveWalletError.DataError -> DataError( IllegalStateException("Failed to update wallet: ${it.messageId}"), ) is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists } }</ID>
|
||||
<ID>NestedScopeFunctions:ColdUserWalletBuilder.kt$ColdUserWalletBuilder$let { UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) }</ID>
|
||||
<ID>NoNameShadowing:SaveWalletUseCase.kt$SaveWalletUseCase$userWallet</ID>
|
||||
<ID>RedundantSuspendModifier:GetHotWalletContextualUnlockUseCase.kt$GetHotWalletContextualUnlockUseCase$suspend</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:RenameWalletUseCase.kt$RenameWalletUseCase$runCatching</ID>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.wallets.builder
|
||||
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
|
|
@ -11,6 +12,7 @@ import dagger.assisted.AssistedInject
|
|||
class ColdUserWalletBuilder @AssistedInject constructor(
|
||||
@Assisted private val scanResponse: ScanResponse,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
) {
|
||||
private var backupCardsIds: Set<String> = emptySet()
|
||||
private var hasBackupError: Boolean = false
|
||||
|
|
@ -32,25 +34,22 @@ class ColdUserWalletBuilder @AssistedInject constructor(
|
|||
this.hasBackupError = hasBackupError
|
||||
}
|
||||
|
||||
fun build(): UserWallet.Cold? {
|
||||
return with(scanResponse) {
|
||||
UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
?.let {
|
||||
UserWallet.Cold(
|
||||
walletId = it,
|
||||
name = generateWalletNameUseCase(
|
||||
card = card,
|
||||
productType = productType,
|
||||
isStartToCoin = cardTypesResolver.isStart2Coin(),
|
||||
),
|
||||
cardsInWallet = backupCardsIds.plus(card.cardId),
|
||||
scanResponse = this,
|
||||
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),
|
||||
hasBackupError = hasBackupError,
|
||||
)
|
||||
}
|
||||
}
|
||||
suspend fun build(): UserWallet.Cold? {
|
||||
val walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
?: return null
|
||||
|
||||
return UserWallet.Cold(
|
||||
walletId = walletId,
|
||||
name = generateWalletNameUseCase(
|
||||
card = scanResponse.card,
|
||||
productType = scanResponse.productType,
|
||||
isStartToCoin = scanResponse.cardTypesResolver.isStart2Coin(),
|
||||
),
|
||||
cardsInWallet = backupCardsIds.plus(scanResponse.card.cardId),
|
||||
scanResponse = scanResponse,
|
||||
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),
|
||||
hasBackupError = hasBackupError || cardRepository.hasBackupError(scanResponse.card.cardId),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
package com.tangem.domain.wallets.builder
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
/**
|
||||
* Tests for [ColdUserWalletBuilder].
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ColdUserWalletBuilderTest {
|
||||
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase = mockk()
|
||||
private val cardRepository: CardRepository = mockk()
|
||||
|
||||
private val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = emptyMap(),
|
||||
)
|
||||
|
||||
private val primaryCardId = scanResponse.card.cardId
|
||||
private val expectedWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(generateWalletNameUseCase, cardRepository)
|
||||
every { generateWalletNameUseCase(any(), any(), any()) } returns WALLET_NAME
|
||||
coEvery { cardRepository.hasBackupError(any()) } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid scan response WHEN build THEN returns cold wallet with expected fields`() = runTest {
|
||||
// Act
|
||||
val result = createBuilder().build()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
UserWallet.Cold(
|
||||
name = WALLET_NAME,
|
||||
walletId = requireNotNull(expectedWalletId),
|
||||
cardsInWallet = setOf(primaryCardId),
|
||||
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),
|
||||
hasBackupError = false,
|
||||
scanResponse = scanResponse,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backup card ids WHEN build THEN cards in wallet include primary and backup`() = runTest {
|
||||
// Act
|
||||
val result = createBuilder()
|
||||
.backupCardsIds(setOf("backup-1", "backup-2"))
|
||||
.build()
|
||||
|
||||
// Assert
|
||||
assertThat(result?.cardsInWallet).containsExactly(primaryCardId, "backup-1", "backup-2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null backup card ids WHEN build THEN cards in wallet contain only primary`() = runTest {
|
||||
// Act
|
||||
val result = createBuilder()
|
||||
.backupCardsIds(null)
|
||||
.build()
|
||||
|
||||
// Assert
|
||||
assertThat(result?.cardsInWallet).containsExactly(primaryCardId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN scan response without wallets WHEN build THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
val scanResponseWithoutWallets = scanResponse.copy(
|
||||
card = scanResponse.card.copy(wallets = emptyList()),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = createBuilder(scanResponseWithoutWallets).build()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN scan response WHEN build THEN wallet name generated from resolver data`() = runTest {
|
||||
// Act
|
||||
createBuilder().build()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) {
|
||||
generateWalletNameUseCase(
|
||||
scanResponse.productType,
|
||||
scanResponse.card,
|
||||
scanResponse.cardTypesResolver.isStart2Coin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun hasBackupError(model: HasBackupErrorModel) = runTest {
|
||||
// Arrange
|
||||
coEvery { cardRepository.hasBackupError(primaryCardId) } returns model.repositoryReturns
|
||||
|
||||
// Act
|
||||
val result = createBuilder()
|
||||
.hasBackupError(model.builderFlag)
|
||||
.build()
|
||||
|
||||
// Assert
|
||||
assertThat(result?.hasBackupError).isEqualTo(model.expected)
|
||||
// The externally set flag short-circuits `||`: the repository is queried only when the flag is not set.
|
||||
coVerify(exactly = model.expectedRepositoryCalls) { cardRepository.hasBackupError(primaryCardId) }
|
||||
}
|
||||
|
||||
private fun createBuilder(scanResponse: ScanResponse = this.scanResponse) = ColdUserWalletBuilder(
|
||||
scanResponse = scanResponse,
|
||||
generateWalletNameUseCase = generateWalletNameUseCase,
|
||||
cardRepository = cardRepository,
|
||||
)
|
||||
|
||||
internal data class HasBackupErrorModel(
|
||||
val builderFlag: Boolean,
|
||||
val repositoryReturns: Boolean,
|
||||
val expected: Boolean,
|
||||
val expectedRepositoryCalls: Int,
|
||||
)
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// Flag not set externally -> repository is queried and is the source of truth.
|
||||
HasBackupErrorModel(
|
||||
builderFlag = false,
|
||||
repositoryReturns = false,
|
||||
expected = false,
|
||||
expectedRepositoryCalls = 1,
|
||||
),
|
||||
HasBackupErrorModel(
|
||||
builderFlag = false,
|
||||
repositoryReturns = true,
|
||||
expected = true,
|
||||
expectedRepositoryCalls = 1,
|
||||
),
|
||||
// Flag set externally -> `||` short-circuits, repository is NOT queried regardless of its value.
|
||||
HasBackupErrorModel(
|
||||
builderFlag = true,
|
||||
repositoryReturns = false,
|
||||
expected = true,
|
||||
expectedRepositoryCalls = 0,
|
||||
),
|
||||
HasBackupErrorModel(builderFlag = true, repositoryReturns = true, expected = true, expectedRepositoryCalls = 0),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val WALLET_NAME = "Wallet"
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ internal class CreateWalletStartModelTest {
|
|||
coEvery { appsFlyerStore.get() } returns null
|
||||
coEvery { settingsRepository.shouldSaveAccessCodes() } returns false
|
||||
every { coldUserWalletBuilderFactory.create(any()) } returns coldUserWalletBuilder
|
||||
every { coldUserWalletBuilder.build() } returns testColdWallet
|
||||
coEvery { coldUserWalletBuilder.build() } returns testColdWallet
|
||||
every { onboardingV2FeatureToggles.isAddressSyncEnabled } returns false
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
|
|
@ -356,7 +356,7 @@ internal class CreateWalletStartModelTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN builder returns null WHEN proceedWithScanResponse THEN saveWalletUseCase not called`() = runTest {
|
||||
every { coldUserWalletBuilder.build() } returns null
|
||||
coEvery { coldUserWalletBuilder.build() } returns null
|
||||
coEvery {
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = any(),
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
|
||||
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = response).build()
|
||||
|
||||
return ensureNotNull(userWallet) { Error.Unknown }
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ internal class UserWalletSaverTest {
|
|||
|
||||
private fun mockBuilderReturns(userWallet: UserWallet.Cold?) {
|
||||
val builder: ColdUserWalletBuilder = mockk {
|
||||
every { build() } returns userWallet
|
||||
coEvery { build() } returns userWallet
|
||||
}
|
||||
every { coldUserWalletBuilderFactory.create(scanResponse = any()) } returns builder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
return requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
|
|
|
|||
|
|
@ -334,13 +334,16 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
// to prevent showing finalize screen dialog on next app start
|
||||
onboardingRepository.clearUnfinishedFinalizeOnboarding()
|
||||
|
||||
cardRepository.finishCardActivation(scanResponse.card.cardId)
|
||||
cardRepository.finishCardActivation(
|
||||
cardId = scanResponse.card.cardId,
|
||||
hasBackupError = hasWalletBackupError,
|
||||
)
|
||||
backupServiceHolder.backupService.get()?.discardSavedBackup()
|
||||
onEvent.emit(MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
return requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse)
|
||||
.backupCardsIds(backupCardIds.toSet())
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
return requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
|
||||
return requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue