Updated on 2026-08-14
This commit is contained in:
parent
a5a8cee12c
commit
9b1bf5813f
15 changed files with 895 additions and 30 deletions
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.common.test.domain.wallet
|
||||
|
||||
import com.tangem.common.card.WalletData
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
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.UserWalletIdBuilder
|
||||
|
||||
/**
|
||||
|
|
@ -29,4 +32,29 @@ object MockUserWalletFactory {
|
|||
hasBackupError = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSingleWalletWithToken(): UserWallet.Cold {
|
||||
return UserWallet.Cold(
|
||||
name = "NODL",
|
||||
walletId = UserWalletId("011"),
|
||||
cardsInWallet = setOf(),
|
||||
isMultiCurrency = false,
|
||||
scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(maxWalletCount = 2),
|
||||
derivedKeys = emptyMap(),
|
||||
).copy(
|
||||
productType = ProductType.Note,
|
||||
walletData = WalletData(
|
||||
blockchain = "ETH",
|
||||
token = WalletData.Token(
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
contractAddress = "0x",
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
hasBackupError = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
|
||||
// region Project - Domain
|
||||
api(projects.domain.account)
|
||||
api(projects.domain.card)
|
||||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.data.account.producer.DefaultMultiAccountListProducer
|
||||
import com.tangem.data.account.producer.DefaultSingleAccountListProducer
|
||||
import com.tangem.domain.account.producer.MultiAccountListProducer
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AccountListProducerFactoryModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleAccountListProducerFactory(
|
||||
impl: DefaultSingleAccountListProducer.Factory,
|
||||
): SingleAccountListProducer.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMultiAccountListProducerFactory(
|
||||
impl: DefaultMultiAccountListProducer.Factory,
|
||||
): MultiAccountListProducer.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.domain.account.producer.MultiAccountListProducer
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AccountListSupplierModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSingleAccountListSupplier(factory: SingleAccountListProducer.Factory): SingleAccountListSupplier {
|
||||
return object : SingleAccountListSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "single_account_list_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiNetworkStatusSupplier(factory: MultiAccountListProducer.Factory): MultiAccountListSupplier {
|
||||
return object : MultiAccountListSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_networks_statuses" },
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.MultiAccountListProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiAccountListProducer].
|
||||
* Produces a list of [AccountList]s for all user wallets.
|
||||
*
|
||||
* @property params params
|
||||
* @property userWalletsStore store that provides user wallets
|
||||
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiAccountListProducer @AssistedInject constructor(
|
||||
@Assisted val params: Unit,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiAccountListProducer {
|
||||
|
||||
override val fallback: Option<List<AccountList>> = emptyList<AccountList>().some()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun produce(): Flow<List<AccountList>> {
|
||||
return userWalletsStore.userWallets
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { userWallets ->
|
||||
combine(
|
||||
flows = userWallets.map(walletAccountListFlowFactory::create),
|
||||
transform = ::listOf,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MultiAccountListProducer.Factory {
|
||||
override fun create(params: Unit): DefaultMultiAccountListProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleAccountListProducer].
|
||||
* Produces a list of [AccountList] for a specific user wallet.
|
||||
*
|
||||
* @property params params containing the user wallet ID
|
||||
* @property userWalletsStore store that provides user wallets
|
||||
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
||||
@Assisted val params: SingleAccountListProducer.Params,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleAccountListProducer {
|
||||
|
||||
override val fallback: Option<AccountList> = none()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun produce(): Flow<AccountList> {
|
||||
return userWalletsStore.userWallets
|
||||
.mapNotNull { userWallets ->
|
||||
userWallets.firstOrNull { it.walletId == params.userWalletId }
|
||||
}
|
||||
.flatMapLatest(walletAccountListFlowFactory::create)
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SingleAccountListProducer.Factory {
|
||||
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import com.tangem.data.account.converter.AccountListConverter
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Factory that creates a flow of [AccountList] for a specific [UserWallet]
|
||||
*
|
||||
* @property accountsResponseStoreFactory factory to create [AccountsResponseStore]
|
||||
* @property accountListConverterFactory factory to create [AccountListConverter]
|
||||
* @property cardCryptoCurrencyFactory factory to create supported crypto currencies for a card
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class WalletAccountListFlowFactory @Inject constructor(
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val accountListConverterFactory: AccountListConverter.Factory,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
) {
|
||||
|
||||
fun create(userWallet: UserWallet): Flow<AccountList> {
|
||||
return if (userWallet.isMultiCurrency) {
|
||||
createForMultiWallet(userWallet)
|
||||
} else {
|
||||
flowOf(createForSingleWallet(userWallet))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createForMultiWallet(userWallet: UserWallet): Flow<AccountList> {
|
||||
val converter by lazy { accountListConverterFactory.create(userWallet) }
|
||||
|
||||
return accountsResponseStoreFactory.create(userWallet.walletId).data
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.map(converter::convert)
|
||||
}
|
||||
|
||||
private fun createForSingleWallet(userWallet: UserWallet): AccountList {
|
||||
val isSingleWalletWithToken = userWallet.requireColdWallet().cardTypesResolver.isSingleWalletWithToken()
|
||||
|
||||
val currencies = if (isSingleWalletWithToken) {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet).toSet()
|
||||
} else {
|
||||
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet).let(::setOf)
|
||||
}
|
||||
|
||||
return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultMultiAccountListProducerTest {
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
|
||||
|
||||
private val producer = DefaultMultiAccountListProducer(
|
||||
params = Unit,
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(userWalletsStore, walletAccountListFlowFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun produce() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(accountList)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if factoryFlow is updated`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
|
||||
val factoryFlow = MutableStateFlow<AccountList?>(null)
|
||||
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
|
||||
|
||||
// Act (first emission)
|
||||
factoryFlow.value = accountList
|
||||
val firstEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
Truth.assertThat(firstEmission).containsExactly(listOf(accountList))
|
||||
|
||||
// Act (second emission)
|
||||
factoryFlow.value = updatedAccountList
|
||||
val secondEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
Truth.assertThat(secondEmission).containsExactly(listOf(updatedAccountList))
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is filtered the same response`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val factoryFlow = MutableStateFlow<AccountList?>(null)
|
||||
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
|
||||
|
||||
// Act (first emission)
|
||||
factoryFlow.value = accountList
|
||||
val firstEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
Truth.assertThat(firstEmission).containsExactly(listOf(accountList))
|
||||
|
||||
// Act (second emission) - the same status
|
||||
factoryFlow.value = accountList
|
||||
val secondEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
Truth.assertThat(secondEmission).containsExactly(listOf(accountList))
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow returns empty list if factory throws exception`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val exception = RuntimeException("Converter error")
|
||||
every { walletAccountListFlowFactory.create(userWallet) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = producer.produceWithFallback().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = emptyList<AccountList>()
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if userWalletsFlow returns empty flow`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = emptyFlow<List<UserWallet>>()
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.userWallets }
|
||||
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if factory returns empty flow`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns emptyFlow()
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if one of factoryFlow is empty`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId2 = UserWalletId("012")
|
||||
val userWallet2 = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId2
|
||||
}
|
||||
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
|
||||
every { walletAccountListFlowFactory.create(userWallet2) } returns emptyFlow()
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
walletAccountListFlowFactory.create(userWallet2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultSingleAccountListProducerTest {
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
private val producer = DefaultSingleAccountListProducer(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(userWalletsStore, walletAccountListFlowFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun produce() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = accountList
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if factoryFlow is updated`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
|
||||
val factoryFlow = MutableStateFlow<AccountList?>(null)
|
||||
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
|
||||
|
||||
// Act (first emission)
|
||||
factoryFlow.value = accountList
|
||||
val firstEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
Truth.assertThat(firstEmission).containsExactly(accountList)
|
||||
|
||||
// Act (second emission)
|
||||
factoryFlow.value = updatedAccountList
|
||||
val secondEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
Truth.assertThat(secondEmission).containsExactly(updatedAccountList)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is filtered the same response`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val factoryFlow = MutableStateFlow<AccountList?>(null)
|
||||
|
||||
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
|
||||
|
||||
// Act (first emission)
|
||||
factoryFlow.value = accountList
|
||||
val firstEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (first emission)
|
||||
Truth.assertThat(firstEmission).containsExactly(accountList)
|
||||
|
||||
// Act (second emission) - the same status
|
||||
factoryFlow.value = accountList
|
||||
val secondEmission = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert (second emission)
|
||||
Truth.assertThat(secondEmission).containsExactly(accountList)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if factory throws exception`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
val exception = RuntimeException("Converter error")
|
||||
every { walletAccountListFlowFactory.create(userWallet) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = producer.produceWithFallback().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
walletAccountListFlowFactory.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if userWalletsFlow returns empty flow`() = runTest {
|
||||
// Arrange
|
||||
val userWalletsFlow = emptyFlow<List<UserWallet>>()
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.userWallets }
|
||||
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if userWalletsFlow doesn't contains userWalletId from params`() = runTest {
|
||||
// Arrange
|
||||
val unknownId = UserWalletId("012")
|
||||
val unknownWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns unknownId
|
||||
}
|
||||
|
||||
val userWalletsFlow = MutableStateFlow(listOf(unknownWallet))
|
||||
every { userWalletsStore.userWallets } returns userWalletsFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // no emissions
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.userWallets
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.data.account.converter.AccountListConverter
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class WalletAccountListFlowFactoryTest {
|
||||
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
|
||||
private val accountsResponseStore: AccountsResponseStore = mockk()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
|
||||
private val accountListConverterFactory: AccountListConverter.Factory = mockk()
|
||||
private val accountListConverter: AccountListConverter = mockk()
|
||||
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
|
||||
private val factory = WalletAccountListFlowFactory(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
accountListConverterFactory = accountListConverterFactory,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(accountListConverter)
|
||||
|
||||
accountsResponseStoreFlow.value = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create for multi wallet`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.isMultiCurrency } returns true
|
||||
}
|
||||
|
||||
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
accountsResponseStoreFlow.value = accountsResponse
|
||||
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
every { accountListConverterFactory.create(userWallet) } returns accountListConverter
|
||||
every { accountListConverter.convert(accountsResponse) } returns accountList
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = accountList
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
accountListConverterFactory.create(userWallet)
|
||||
accountListConverter.convert(accountsResponse)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(any())
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create for single wallet`() = runTest {
|
||||
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
|
||||
|
||||
val currency = cryptoCurrencyFactory.ethereum
|
||||
every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency
|
||||
|
||||
// Act
|
||||
val actual = factory.create(userWallet).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountList.empty(userWallet = userWallet, cryptoCurrencies = setOf(currency))
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = any())
|
||||
accountsResponseStoreFactory.create(any())
|
||||
accountsResponseStore.data
|
||||
accountListConverterFactory.create(any())
|
||||
accountListConverter.convert(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is created for single wallet with token`() = runTest {
|
||||
val nodl = MockUserWalletFactory.createSingleWalletWithToken()
|
||||
|
||||
val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet()
|
||||
every {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl)
|
||||
} returns currencies.toList()
|
||||
|
||||
// Act
|
||||
val actual = factory.create(nodl).let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = AccountList.empty(userWallet = nodl, cryptoCurrencies = currencies)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
|
||||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(any())
|
||||
accountsResponseStoreFactory.create(any())
|
||||
accountsResponseStore.data
|
||||
accountListConverterFactory.create(any())
|
||||
accountListConverter.convert(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,14 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.common.card.WalletData
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -159,7 +160,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
model: CreateTestModel.SingleWalletWithToken,
|
||||
) = runTest {
|
||||
// Arrange
|
||||
val userWallet = createSingleWalletWithToken()
|
||||
val userWallet = MockUserWalletFactory.createSingleWalletWithToken()
|
||||
|
||||
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
|
||||
|
||||
|
|
@ -284,7 +285,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
|
||||
),
|
||||
CreateCurrenciesForMultiWalletModel(
|
||||
multiWallet = createSingleWalletWithToken(),
|
||||
multiWallet = MockUserWalletFactory.createSingleWalletWithToken(),
|
||||
userTokensResponse = null,
|
||||
expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")),
|
||||
),
|
||||
|
|
@ -456,7 +457,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")),
|
||||
),
|
||||
CreateForSingleWalletWithTokenModel(
|
||||
singleWalletWithToken = createSingleWalletWithToken(),
|
||||
singleWalletWithToken = MockUserWalletFactory.createSingleWalletWithToken(),
|
||||
isPrimaryTokenExpected = true,
|
||||
expected = Result.success(listOf(ethereum)),
|
||||
),
|
||||
|
|
@ -522,33 +523,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createSingleWalletWithToken(): UserWallet.Cold {
|
||||
return UserWallet.Cold(
|
||||
name = "NODL",
|
||||
walletId = UserWalletId("011"),
|
||||
cardsInWallet = setOf(),
|
||||
isMultiCurrency = false,
|
||||
scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(maxWalletCount = 2),
|
||||
derivedKeys = emptyMap(),
|
||||
).copy(
|
||||
productType = ProductType.Note,
|
||||
walletData = WalletData(
|
||||
blockchain = "ETH",
|
||||
token = WalletData.Token(
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
contractAddress = "0x",
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
hasBackupError = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPrimaryToken(blockchain: Blockchain): CryptoCurrency.Token {
|
||||
val userWallet = createSingleWalletWithToken()
|
||||
val userWallet = MockUserWalletFactory.createSingleWalletWithToken()
|
||||
|
||||
return CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
|
||||
sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.account.producer
|
||||
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.core.flow.FlowProducer
|
||||
|
||||
/**
|
||||
* Produces a list of [AccountList]s for all user wallets.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MultiAccountListProducer : FlowProducer<List<AccountList>> {
|
||||
|
||||
interface Factory : FlowProducer.Factory<Unit, MultiAccountListProducer>
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.account.producer
|
||||
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.core.flow.FlowProducer
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Produces a list of [AccountList] for a specific user wallet.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SingleAccountListProducer : FlowProducer<AccountList> {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : FlowProducer.Factory<Params, SingleAccountListProducer>
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.account.supplier
|
||||
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.MultiAccountListProducer
|
||||
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Supplier that provides a list of [AccountList]s for all user wallets.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class MultiAccountListSupplier(
|
||||
override val factory: MultiAccountListProducer.Factory,
|
||||
override val keyCreator: (Unit) -> String,
|
||||
) : FlowCachingSupplier<MultiAccountListProducer, Unit, List<AccountList>>() {
|
||||
|
||||
operator fun invoke(): Flow<List<AccountList>> {
|
||||
return super.invoke(params = Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.account.supplier
|
||||
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||
|
||||
/**
|
||||
* Supplier that provides a single [AccountList] for a specific user wallet.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class SingleAccountListSupplier(
|
||||
override val factory: SingleAccountListProducer.Factory,
|
||||
override val keyCreator: (SingleAccountListProducer.Params) -> String,
|
||||
) : FlowCachingSupplier<SingleAccountListProducer, SingleAccountListProducer.Params, AccountList>()
|
||||
Loading…
Add table
Add a link
Reference in a new issue