From 247dd6b099c471b5fa6764f298698091cefa96a6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 14:16:18 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../foryou/impl/model/ForYouModelTest.kt | 346 ++++++++++++++++++ .../ForYouPortfolioFormattersTest.kt | 163 +++++++++ .../converter/ForYouTokenListConverterTest.kt | 285 +++++++++++++++ .../converter/ForYouTokenRowConverterTest.kt | 131 +++++++ .../SetPortfolioReviewTransformerTest.kt | 243 ++++++++++++ 5 files changed, 1168 insertions(+) create mode 100644 features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/ForYouModelTest.kt create mode 100644 features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouPortfolioFormattersTest.kt create mode 100644 features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenListConverterTest.kt create mode 100644 features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenRowConverterTest.kt create mode 100644 features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/transformer/SetPortfolioReviewTransformerTest.kt diff --git a/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/ForYouModelTest.kt b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/ForYouModelTest.kt new file mode 100644 index 0000000000..b538927ce3 --- /dev/null +++ b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/ForYouModelTest.kt @@ -0,0 +1,346 @@ +package com.tangem.features.foryou.impl.model + +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletIcon +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase +import com.tangem.features.foryou.impl.entity.PortfolioReviewUM +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +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.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("LargeClass") +internal class ForYouModelTest { + + private val userWalletsListRepository: UserWalletsListRepository = mockk() + private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier = mockk() + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk() + private val walletIconUMConverter: WalletIconUMConverter = mockk { + every { convert(any()) } returns DeviceIconUM.Stub(cardsCount = 1) + } + private val getWalletIconUseCase: GetWalletIconUseCase = mockk() + + private var model: ForYouModel? = null + + @BeforeEach + fun setup() { + every { getWalletIconUseCase.invoke(any()) } returns UserWalletIcon.Stub(cardsCount = 1) + // Default: a real, non-empty emission so the model's `getOrElse { Default }` mapping path is + // actually exercised in every test, not bypassed by an empty flow. Individual tests may override. + every { getSelectedAppCurrencyUseCase() } returns flowOf(AppCurrency.Default.right()) + } + + @AfterEach + fun tearDown() { + model?.onDestroy() + model = null + } + + @Nested + inner class InitialState { + + @Test + fun `GIVEN model created WHEN not yet advanced THEN uiState is Loading`() = runTest { + // Arrange + every { userWalletsListRepository.userWallets } returns MutableStateFlow(null) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(null) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf(linkedMapOf()) + every { getSelectedAppCurrencyUseCase() } returns flowOf(AppCurrency.Default.right()) + + // Act + val model = createModel(testScope = this) + + // Assert — before advancing, the model exposes skeleton placeholder rows + val loading = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Loading + + assertThat(loading.tokenList).hasSize(4) + assertThat(loading.tokenList.all { it.tokenRowUM is TangemTokenRowUM.Loading }).isTrue() + } + } + + @Nested + inner class ContentState { + + @Test + fun `GIVEN wallets and account statuses emitted WHEN advanced THEN uiState becomes Content with tabs`() = + runTest { + // Arrange + val walletOne = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1") + val walletTwo = MockUserWalletFactory.create().copy(walletId = UserWalletId("02"), name = "Wallet 2") + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(walletOne, walletTwo)) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(walletOne) + + val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC") + val accountStatusList = createAccountStatusList( + userWalletId = walletOne.walletId, + currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))), + totalFiatBalance = BigDecimal("100"), + ) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf( + linkedMapOf(walletOne.walletId to accountStatusList), + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf() + + // Act + val model = createModel(testScope = this) + advanceUntilIdle() + + // Assert + val content = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + assertThat(content.assetCount).isNotNull() + assertThat(model.uiState.value.walletListUM.items).hasSize(2) + assertThat(model.uiState.value.walletListUM.items.map { it.isSelected }).containsExactly(true, false) + } + + @Test + fun `GIVEN exactly one wallet WHEN advanced THEN walletListUM items is empty`() = runTest { + // Arrange + val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1") + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet) + + val accountStatusList = createAccountStatusList( + userWalletId = wallet.walletId, + currencies = emptyList(), + totalFiatBalance = BigDecimal.ZERO, + ) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf( + linkedMapOf(wallet.walletId to accountStatusList), + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf() + + // Act + val model = createModel(testScope = this) + advanceUntilIdle() + + // Assert — the "tabs.size != 1" rule: a single wallet shows no tabs + assertThat(model.uiState.value.walletListUM.items).isEmpty() + } + } + + @Nested + inner class TabClick { + + @Test + fun `GIVEN two wallets WHEN onTabClick THEN locally selected wallet switches and currencies rederive`() = + runTest { + // Arrange + val walletOne = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1") + val walletTwo = MockUserWalletFactory.create().copy(walletId = UserWalletId("02"), name = "Wallet 2") + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(walletOne, walletTwo)) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(walletOne) + + val btc = createCoin(rawCurrencyId = "btc", symbol = "BTC") + val eth = createCoin(rawCurrencyId = "eth", symbol = "ETH") + val statusOne = createAccountStatusList( + userWalletId = walletOne.walletId, + currencies = listOf(createStatus(btc, loadedValue(BigDecimal("100")))), + totalFiatBalance = BigDecimal("100"), + ) + val statusTwo = createAccountStatusList( + userWalletId = walletTwo.walletId, + currencies = listOf(createStatus(eth, loadedValue(BigDecimal("50")))), + totalFiatBalance = BigDecimal("50"), + ) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf( + linkedMapOf(walletOne.walletId to statusOne, walletTwo.walletId to statusTwo), + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf() + + val model = createModel(testScope = this) + advanceUntilIdle() + + // Act — click the second wallet's tab + model.uiState.value.walletListUM.items[1].onClick() + advanceUntilIdle() + + // Assert + assertThat(model.uiState.value.walletListUM.items.map { it.isSelected }).containsExactly( + false, + true, + ).inOrder() + val content = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + val assetRow = content.tokenList.single().tokenRowUM as TangemTokenRowUM.Content + val titleUM = assetRow.titleUM as TangemTokenRowUM.TitleUM.Content + assertThat(titleUM.text).isEqualTo(com.tangem.core.ui.extensions.stringReference("ETH")) + } + } + + @Nested + inner class ExpandClick { + + @Test + fun `GIVEN asset row clicked WHEN clicked again THEN isExpanded toggles back to false`() = runTest { + // Arrange + val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1") + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet) + + val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC") + val accountStatusList = createAccountStatusList( + userWalletId = wallet.walletId, + currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))), + totalFiatBalance = BigDecimal("100"), + ) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf( + linkedMapOf(wallet.walletId to accountStatusList), + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf() + + val model = createModel(testScope = this) + advanceUntilIdle() + val initialContent = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + assertThat(initialContent.tokenList.single().isExpanded).isFalse() + + // Act — click once to expand + val assetRow = initialContent.tokenList.single().tokenRowUM as TangemTokenRowUM.Content + assetRow.onItemClick?.invoke() + advanceUntilIdle() + + // Assert + val expandedContent = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + assertThat(expandedContent.tokenList.single().isExpanded).isTrue() + + // Act — click again to collapse + val expandedRow = expandedContent.tokenList.single().tokenRowUM as TangemTokenRowUM.Content + expandedRow.onItemClick?.invoke() + advanceUntilIdle() + + // Assert + val collapsedContent = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + assertThat(collapsedContent.tokenList.single().isExpanded).isFalse() + } + } + + @Nested + inner class PeriodClick { + + @Test + fun `GIVEN Content state WHEN period clicked THEN initialSelectedItem updates without resetting rest`() = + runTest { + // Arrange + val wallet = MockUserWalletFactory.create().copy(walletId = UserWalletId("01"), name = "Wallet 1") + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet) + + val currency = createCoin(rawCurrencyId = "btc", symbol = "BTC") + val accountStatusList = createAccountStatusList( + userWalletId = wallet.walletId, + currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))), + totalFiatBalance = BigDecimal("100"), + ) + every { multiAccountStatusListSupplier.invokeAsMap() } returns flowOf( + linkedMapOf(wallet.walletId to accountStatusList), + ) + every { getSelectedAppCurrencyUseCase() } returns flowOf() + + val model = createModel(testScope = this) + advanceUntilIdle() + val contentBefore = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + val weekItem = contentBefore.periodPickerUM.items[1] + val assetCountBefore = contentBefore.assetCount + + // Act + contentBefore.onPeriodClick(weekItem) + + // Assert + val contentAfter = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Content + assertThat(contentAfter.periodPickerUM.initialSelectedItem).isEqualTo(weekItem) + assertThat(contentAfter.assetCount).isEqualTo(assetCountBefore) + assertThat(contentAfter.tokenList).isEqualTo(contentBefore.tokenList) + } + } + + private fun createModel(testScope: TestScope): ForYouModel { + return ForYouModel( + userWalletsListRepository = userWalletsListRepository, + multiAccountStatusListSupplier = multiAccountStatusListSupplier, + dispatchers = testScope.createTestingCoroutineDispatcherProvider(), + getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + walletIconUMConverter = walletIconUMConverter, + getWalletIconUseCase = getWalletIconUseCase, + ).also { model = it } + } + + private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider { + val testDispatcher = StandardTestDispatcher(testScheduler) + return TestingCoroutineDispatcherProvider( + main = testDispatcher, + mainImmediate = testDispatcher, + io = testDispatcher, + default = testDispatcher, + single = testDispatcher, + ) + } + + private fun createAccountStatusList( + userWalletId: UserWalletId, + currencies: List, + totalFiatBalance: BigDecimal, + ): AccountStatusList = mockk { + every { this@mockk.userWalletId } returns userWalletId + every { flattenCurrencies() } returns currencies + every { this@mockk.totalFiatBalance } returns TotalFiatBalance.Loaded( + amount = totalFiatBalance, + source = com.tangem.domain.models.StatusSource.ACTUAL, + ) + } + + private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus( + currency = currency, + value = value, + ) + + private fun loadedValue(fiatAmount: BigDecimal): CryptoCurrencyStatus.Loaded = mockk { + every { amount } returns BigDecimal.ONE + every { this@mockk.fiatAmount } returns fiatAmount + every { isError } returns false + } + + private fun createCoin(rawCurrencyId: String, symbol: String): CryptoCurrency.Coin { + val network: Network = mockk { + every { name } returns "Network" + every { isTestnet } returns false + every { id } returns mockk { every { rawId } returns Network.RawID(rawCurrencyId) } + } + val currencyId: CryptoCurrency.ID = mockk { + every { value } returns "coin-$rawCurrencyId" + every { this@mockk.rawCurrencyId } returns CryptoCurrency.RawID(rawCurrencyId) + } + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.symbol } returns symbol + every { this@mockk.network } returns network + every { this@mockk.decimals } returns 8 + every { isCustom } returns false + every { iconUrl } returns null + } + } +} \ No newline at end of file diff --git a/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouPortfolioFormattersTest.kt b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouPortfolioFormattersTest.kt new file mode 100644 index 0000000000..9bc177f833 --- /dev/null +++ b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouPortfolioFormattersTest.kt @@ -0,0 +1,163 @@ +package com.tangem.features.foryou.impl.model.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class ForYouPortfolioFormattersTest { + + private val appCurrency: AppCurrency = AppCurrency.Default + + @Nested + inner class ForYouGroupKey { + + @Test + fun `GIVEN standard currency with raw id WHEN forYouGroupKey THEN returns rawCurrencyId value`() { + // Arrange + val id: CryptoCurrency.ID = mockk { + every { rawCurrencyId } returns CryptoCurrency.RawID("bitcoin") + every { value } returns "coin-id-value" + } + val currency: CryptoCurrency = mockk { every { this@mockk.id } returns id } + val status = createStatus(currency) + + // Act + val result = status.forYouGroupKey() + + // Assert + assertThat(result).isEqualTo("bitcoin") + } + + @Test + fun `GIVEN custom token with no raw id WHEN forYouGroupKey THEN falls back to currency id value`() { + // Arrange + val id: CryptoCurrency.ID = mockk { + every { rawCurrencyId } returns null + every { value } returns "custom-currency-id" + } + val currency: CryptoCurrency = mockk { every { this@mockk.id } returns id } + val status = createStatus(currency) + + // Act + val result = status.forYouGroupKey() + + // Assert + assertThat(result).isEqualTo("custom-currency-id") + } + + private fun createStatus(currency: CryptoCurrency): CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loading, + ) + } + + @Nested + inner class ToForYouFiatText { + + @Test + fun `GIVEN a fiat amount WHEN toForYouFiatText THEN delegates to fiat formatting`() { + // Arrange + val amount = BigDecimal("1234.5") + + // Act + val result = amount.toForYouFiatText(appCurrency) + + // Assert + val expected = stringReference( + amount.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `GIVEN null amount WHEN toForYouFiatText THEN renders dash text`() { + // Arrange + val amount: BigDecimal? = null + + // Act + val result = amount.toForYouFiatText(appCurrency) + + // Assert + val expected = stringReference( + amount.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, + ) + assertThat(result).isEqualTo(expected) + } + } + + @Nested + inner class ToForYouPercentText { + + @Test + fun `GIVEN null amount WHEN toForYouPercentText THEN returns EMPTY`() { + // Arrange + val amount: BigDecimal? = null + + // Act + val result = amount.toForYouPercentText(BigDecimal("100")) + + // Assert + assertThat(result).isEqualTo(TextReference.EMPTY) + } + + @Test + fun `GIVEN zero total WHEN toForYouPercentText THEN returns EMPTY`() { + // Arrange + val amount = BigDecimal("10") + + // Act + val result = amount.toForYouPercentText(BigDecimal.ZERO) + + // Assert + assertThat(result).isEqualTo(TextReference.EMPTY) + } + + @Test + fun `GIVEN zero amount WHEN toForYouPercentText THEN returns EMPTY`() { + // Arrange + val amount = BigDecimal.ZERO + + // Act + val result = amount.toForYouPercentText(BigDecimal("100")) + + // Assert + assertThat(result).isEqualTo(TextReference.EMPTY) + } + + @Test + fun `GIVEN non-zero amount and total WHEN toForYouPercentText THEN returns rounded percent share`() { + // Arrange + val amount = BigDecimal("25.00") + val total = BigDecimal("100") + + // Act + val result = amount.toForYouPercentText(total) + + // Assert — 25.00 / 100 = 0.25 -> 25.00% + assertThat(result).isEqualTo(stringReference("25.00%")) + } + + @Test + fun `GIVEN a share requiring rounding WHEN toForYouPercentText THEN applies HALF_UP rounding`() { + // Arrange — 1.0000 / 3 = 0.3333... -> rounds to 33.33% + val amount = BigDecimal("1.0000") + val total = BigDecimal("3") + + // Act + val result = amount.toForYouPercentText(total) + + // Assert + assertThat(result).isEqualTo(stringReference("33.33%")) + } + } +} \ No newline at end of file diff --git a/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenListConverterTest.kt b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenListConverterTest.kt new file mode 100644 index 0000000000..ff86191938 --- /dev/null +++ b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenListConverterTest.kt @@ -0,0 +1,285 @@ +package com.tangem.features.foryou.impl.model.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.features.foryou.impl.R +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class ForYouTokenListConverterTest { + + private val appCurrency: AppCurrency = AppCurrency.Default + + @Nested + inner class Convert { + + @Test + fun `GIVEN single-network coin WHEN convert THEN subtitle is common main network`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter(totalFiatBalance = BigDecimal("100")) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + val row = result.single().tokenRowUM as TangemTokenRowUM.Content + val subtitle = row.subtitleUM as TangemTokenRowUM.SubtitleUM.Content + assertThat(subtitle.text).isEqualTo(resourceReference(R.string.common_main_network)) + } + + @Test + fun `GIVEN single-network token WHEN convert THEN subtitle is the network standard type name`() { + // Arrange + val currency = createToken( + rawCurrencyId = "usdc", + symbol = "USDC", + networkId = "ethereum", + standardTypeName = "ERC20", + ) + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter(totalFiatBalance = BigDecimal("100")) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + val row = result.single().tokenRowUM as TangemTokenRowUM.Content + val subtitle = row.subtitleUM as TangemTokenRowUM.SubtitleUM.Content + assertThat(subtitle.text).isEqualTo(stringReference("ERC20")) + } + + @Test + fun `GIVEN asset spans multiple networks WHEN convert THEN subtitle shows network count`() { + // Arrange — same asset (shared rawCurrencyId) on two different networks + val onEth = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "ethereum") + val onSol = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "solana") + val statusEth = createStatus(onEth, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val statusSol = createStatus(onSol, loadedValue(BigDecimal("2"), BigDecimal("200"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("300"), + ) + + // Act + val result = converter.convert(listOf(statusEth, statusSol)) + + // Assert + val item = result.single() + val row = item.tokenRowUM as TangemTokenRowUM.Content + val subtitle = row.subtitleUM as TangemTokenRowUM.SubtitleUM.Content + assertThat(subtitle.text).isEqualTo(stringReference("2 networks")) + assertThat(item.tokenList).hasSize(2) + } + + @Test + fun `GIVEN multi-network asset WHEN convert THEN child rows ordered by descending fiat balance`() { + // Arrange + val onEth = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "ethereum") + val onSol = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "solana") + val statusEth = createStatus(onEth, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val statusSol = createStatus(onSol, loadedValue(BigDecimal("2"), BigDecimal("500"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("600"), + ) + + // Act + val result = converter.convert(listOf(statusEth, statusSol)) + + // Assert — Solana holding (500) ranks above Ethereum holding (100) + val childIds = result.single().tokenList.map { it.id } + assertThat(childIds).containsExactly("token-usdc-solana", "token-usdc-ethereum").inOrder() + } + + @Test + fun `GIVEN all statuses of an asset are Loading WHEN convert THEN asset row is Loading`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, CryptoCurrencyStatus.Loading) + val converter = createConverter(totalFiatBalance = BigDecimal.ZERO) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + assertThat(result.single().tokenRowUM).isInstanceOf(TangemTokenRowUM.Loading::class.java) + } + + @Test + fun `GIVEN otherAssetCount is zero WHEN convert THEN no Other row is appended`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("100"), + otherAssetCount = 0, + ) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + assertThat(result).hasSize(1) + } + + @Test + fun `GIVEN otherAssetCount is one WHEN convert THEN Other row subtitle is singular`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("100"), + otherAssetCount = 1, + otherFiatBalance = BigDecimal("50"), + ) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + val otherRow = result.last().tokenRowUM as TangemTokenRowUM.Content + assertThat(otherRow.id).isEqualTo("for_you_other_assets") + val subtitle = otherRow.subtitleUM as TangemTokenRowUM.SubtitleUM.Content + assertThat(subtitle.text).isEqualTo(stringReference("1 asset")) + } + + @Test + fun `GIVEN otherAssetCount is more than one WHEN convert THEN Other row subtitle is plural`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("100"), + otherAssetCount = 3, + otherFiatBalance = BigDecimal("50"), + ) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + val otherRow = result.last().tokenRowUM as TangemTokenRowUM.Content + val subtitle = otherRow.subtitleUM as TangemTokenRowUM.SubtitleUM.Content + assertThat(subtitle.text).isEqualTo(stringReference("3 assets")) + } + + @Test + fun `GIVEN asset id in expandedAssetIds WHEN convert THEN item isExpanded is true`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("100"), + expandedAssetIds = setOf("bitcoin"), + ) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + assertThat(result.single().isExpanded).isTrue() + } + + @Test + fun `GIVEN asset id not in expandedAssetIds WHEN convert THEN item isExpanded is false`() { + // Arrange + val currency = createCoin(rawCurrencyId = "bitcoin", symbol = "BTC", networkId = "bitcoin") + val status = createStatus(currency, loadedValue(BigDecimal("1"), BigDecimal("100"))) + val converter = createConverter( + totalFiatBalance = BigDecimal("100"), + expandedAssetIds = emptySet(), + ) + + // Act + val result = converter.convert(listOf(status)) + + // Assert + assertThat(result.single().isExpanded).isFalse() + } + } + + private fun createConverter( + totalFiatBalance: BigDecimal, + expandedAssetIds: Set = emptySet(), + otherAssetCount: Int = 0, + otherFiatBalance: BigDecimal = BigDecimal.ZERO, + ): ForYouTokenListConverter = ForYouTokenListConverter( + appCurrency = appCurrency, + totalFiatBalance = totalFiatBalance, + expandedAssetIds = expandedAssetIds, + expandClick = {}, + otherAssetCount = otherAssetCount, + otherFiatBalance = otherFiatBalance, + ) + + private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus( + currency = currency, + value = value, + ) + + private fun loadedValue(amount: BigDecimal, fiatAmount: BigDecimal): CryptoCurrencyStatus.Loaded = mockk { + every { this@mockk.amount } returns amount + every { this@mockk.fiatAmount } returns fiatAmount + every { isError } returns false + } + + private fun createCoin(rawCurrencyId: String, symbol: String, networkId: String): CryptoCurrency.Coin { + val network = createNetwork(networkId = networkId, standardTypeName = "MAIN") + val currencyId = createCurrencyId(idValue = "coin-$rawCurrencyId-$networkId", rawCurrencyId = rawCurrencyId) + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.symbol } returns symbol + every { this@mockk.network } returns network + every { this@mockk.decimals } returns 8 + every { isCustom } returns false + every { iconUrl } returns null + } + } + + private fun createToken( + rawCurrencyId: String, + symbol: String, + networkId: String, + standardTypeName: String = "ERC20", + ): CryptoCurrency.Token { + val network = createNetwork(networkId = networkId, standardTypeName = standardTypeName) + val currencyId = createCurrencyId(idValue = "token-$rawCurrencyId-$networkId", rawCurrencyId = rawCurrencyId) + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.symbol } returns symbol + every { this@mockk.network } returns network + every { this@mockk.decimals } returns 6 + every { isCustom } returns false + every { iconUrl } returns null + every { contractAddress } returns "0xCONTRACT" + } + } + + private fun createCurrencyId(idValue: String, rawCurrencyId: String): CryptoCurrency.ID = mockk { + every { value } returns idValue + every { this@mockk.rawCurrencyId } returns CryptoCurrency.RawID(rawCurrencyId) + } + + private fun createNetwork(networkId: String, standardTypeName: String): Network { + val standardType: Network.StandardType = mockk { + every { name } returns standardTypeName + } + return mockk { + every { id } returns mockk { + every { rawId } returns Network.RawID(networkId) + } + every { name } returns networkId + every { isTestnet } returns false + every { this@mockk.standardType } returns standardType + } + } +} \ No newline at end of file diff --git a/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenRowConverterTest.kt b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenRowConverterTest.kt new file mode 100644 index 0000000000..c9a0a73940 --- /dev/null +++ b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/converter/ForYouTokenRowConverterTest.kt @@ -0,0 +1,131 @@ +package com.tangem.features.foryou.impl.model.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class ForYouTokenRowConverterTest { + + private val appCurrency: AppCurrency = AppCurrency.Default + + @Nested + inner class ConvertNetworkGroup { + + @Test + fun `GIVEN all statuses Loading WHEN convertNetworkGroup THEN row is Loading with representative id`() { + // Arrange + val currency = createCurrency(id = "coin-eth", symbol = "ETH") + val statuses = listOf(createStatus(currency, CryptoCurrencyStatus.Loading)) + val converter = createConverter(totalFiatBalance = BigDecimal("1000")) + + // Act + val result = converter.convertNetworkGroup(statuses) + + // Assert + assertThat(result).isEqualTo(TangemTokenRowUM.Loading(id = "coin-eth")) + } + + @Test + fun `GIVEN single loaded status WHEN convertNetworkGroup THEN row is Content with its amounts`() { + // Arrange + val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") + val statuses = listOf( + createStatus(currency, loadedValue(amount = BigDecimal("2"), fiatAmount = BigDecimal("400"))), + ) + val converter = createConverter(totalFiatBalance = BigDecimal("1000")) + + // Act + val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content + + // Assert + assertThat(result.id).isEqualTo("coin-eth") + val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content + val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content + assertThat(topEnd.text).isEqualTo(BigDecimal("400").toForYouFiatText(appCurrency)) + assertThat(bottomEnd.text).isEqualTo(BigDecimal("400").toForYouPercentText(BigDecimal("1000"))) + } + + @Test + fun `GIVEN several statuses of the same asset on one network WHEN convertNetworkGroup THEN amounts are summed`() { + // Arrange — same asset held in two accounts on the same network aggregates into one row + val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") + val statuses = listOf( + createStatus(currency, loadedValue(amount = BigDecimal("1"), fiatAmount = BigDecimal("200"))), + createStatus(currency, loadedValue(amount = BigDecimal("2"), fiatAmount = BigDecimal("400"))), + ) + val converter = createConverter(totalFiatBalance = BigDecimal("1000")) + + // Act + val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content + + // Assert + val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content + assertThat(topEnd.text).isEqualTo(BigDecimal("600").toForYouFiatText(appCurrency)) + } + + @Test + fun `GIVEN mixed Loading and Loaded statuses WHEN convertNetworkGroup THEN row is Content`() { + // Arrange — not *all* statuses are Loading, so it should not collapse to a Loading row + val currency = createCurrency(id = "coin-eth", symbol = "ETH") + val statuses = listOf( + createStatus(currency, CryptoCurrencyStatus.Loading), + createStatus(currency, loadedValue(amount = BigDecimal("1"), fiatAmount = BigDecimal("100"))), + ) + val converter = createConverter(totalFiatBalance = BigDecimal("1000")) + + // Act + val result = converter.convertNetworkGroup(statuses) + + // Assert + assertThat(result).isInstanceOf(TangemTokenRowUM.Content::class.java) + } + } + + private fun createConverter(totalFiatBalance: BigDecimal) = ForYouTokenRowConverter( + appCurrency = appCurrency, + totalFiatBalance = totalFiatBalance, + ) + + private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus( + currency = currency, + value = value, + ) + + private fun loadedValue(amount: BigDecimal, fiatAmount: BigDecimal): CryptoCurrencyStatus.Loaded = mockk { + every { this@mockk.amount } returns amount + every { this@mockk.fiatAmount } returns fiatAmount + every { isError } returns false + } + + private fun createCurrency( + id: String, + symbol: String, + networkName: String = "Network", + ): CryptoCurrency { + val network: Network = mockk { + every { name } returns networkName + every { isTestnet } returns false + every { this@mockk.id } returns mockk { every { rawId } returns Network.RawID(id) } + } + val currencyId: CryptoCurrency.ID = mockk { + every { value } returns id + every { rawCurrencyId } returns null + } + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.symbol } returns symbol + every { this@mockk.network } returns network + every { this@mockk.decimals } returns 8 + every { isCustom } returns false + every { iconUrl } returns null + } + } +} \ No newline at end of file diff --git a/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/transformer/SetPortfolioReviewTransformerTest.kt b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/transformer/SetPortfolioReviewTransformerTest.kt new file mode 100644 index 0000000000..76ddeda041 --- /dev/null +++ b/features/for-you/impl/src/test/kotlin/com/tangem/features/foryou/impl/model/transformer/SetPortfolioReviewTransformerTest.kt @@ -0,0 +1,243 @@ +package com.tangem.features.foryou.impl.model.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM +import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM +import com.tangem.features.foryou.impl.entity.ForYouUM +import com.tangem.features.foryou.impl.entity.PortfolioReviewUM +import io.mockk.every +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class SetPortfolioReviewTransformerTest { + + private val appCurrency: AppCurrency = AppCurrency.Default + private val walletListUM = WalletListUM(items = persistentListOf()) + + @Nested + inner class Transform { + + @Test + fun `GIVEN currency with zero fiat balance WHEN transform THEN it is dropped from asset count`() { + // Arrange + val zeroBalance = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val nonZeroBalance = createCurrency(rawCurrencyId = "eth", symbol = "ETH") + val currencies = listOf( + createStatus(zeroBalance, loadedValue(BigDecimal.ZERO)), + createStatus(nonZeroBalance, loadedValue(BigDecimal("100"))), + ) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("100")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.assetCount).isEqualTo(stringReference("1 assets")) + } + + @Test + fun `GIVEN assets across networks WHEN transform THEN they are aggregated and ranked by summed fiat`() { + // Arrange — same asset (rawCurrencyId "usdc") on two networks aggregates into one asset + val onEth = createCurrency(rawCurrencyId = "usdc", symbol = "USDC") + val onSol = createCurrency(rawCurrencyId = "usdc", symbol = "USDC") + val other = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf( + createStatus(onEth, loadedValue(BigDecimal("50"))), + createStatus(onSol, loadedValue(BigDecimal("60"))), + createStatus(other, loadedValue(BigDecimal("10"))), + ) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("120")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert — 2 ranked assets: usdc (110 total) and btc (10) + assertThat(result.assetCount).isEqualTo(stringReference("2 assets")) + } + + @Test + fun `GIVEN more than TOP_HOLDINGS_COUNT assets WHEN transform THEN excess assets collapse into Other`() { + // Arrange — 5 distinct assets, top 4 kept individually, 5th collapsed into "Other" + val currencies = (1..5).map { index -> + createStatus( + createCurrency(rawCurrencyId = "asset-$index", symbol = "A$index"), + loadedValue(BigDecimal(100 - index)), + ) + } + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("470")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert — tokenList has 4 top asset rows + 1 "Other" row = 5 items + assertThat(result.tokenList).hasSize(5) + assertThat(result.tokenList.last().tokenRowUM.id).isEqualTo("for_you_other_assets") + } + + @Test + fun `GIVEN exactly TOP_HOLDINGS_COUNT assets WHEN transform THEN no Other row is appended`() { + // Arrange + val currencies = (1..4).map { index -> + createStatus( + createCurrency(rawCurrencyId = "asset-$index", symbol = "A$index"), + loadedValue(BigDecimal(100 - index)), + ) + } + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("394")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.tokenList).hasSize(4) + } + + @Test + fun `GIVEN total and top balance non-zero WHEN transform THEN topHoldingPercent is computed`() { + // Arrange — a single asset means top balance == total balance == 100% + val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("100")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.topHoldingPercent).isEqualTo(stringReference("Top holding 100.00%")) + } + + @Test + fun `GIVEN zero total fiat balance WHEN transform THEN topHoldingPercent is DASH_SIGN`() { + // Arrange + val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf(createStatus(currency, loadedValue(BigDecimal.ZERO))) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal.ZERO) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.topHoldingPercent).isEqualTo(stringReference("Top holding —")) + } + + @Test + fun `GIVEN prev state is Loading WHEN transform THEN period picker is freshly created with Day selected`() { + // Arrange + val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10")))) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("10")) + + // Act + val result = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.periodPickerUM.items.map { it.title }).containsExactly( + stringReference("Day"), + stringReference("Week"), + stringReference("Month"), + ).inOrder() + assertThat(result.periodPickerUM.initialSelectedItem?.title).isEqualTo(stringReference("Day")) + } + + @Test + fun `GIVEN prev state is Content WHEN transform THEN period picker selection is preserved`() { + // Arrange + val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10")))) + val transformer = createTransformer(currencies = currencies, totalFiatBalance = BigDecimal("10")) + val prevContentState = transformer.transform(loadingState()).portfolioReviewUM as PortfolioReviewUM.Content + val weekItem = prevContentState.periodPickerUM.items[1] + val prevWithWeekSelected = prevContentState.copy( + periodPickerUM = prevContentState.periodPickerUM.copy(initialSelectedItem = weekItem), + ) + val prevState = ForYouUM(walletListUM = walletListUM, portfolioReviewUM = prevWithWeekSelected) + + // Act + val result = transformer.transform(prevState).portfolioReviewUM as PortfolioReviewUM.Content + + // Assert + assertThat(result.periodPickerUM.initialSelectedItem).isEqualTo(weekItem) + } + + @Test + fun `GIVEN new state WHEN transform THEN walletListUM is applied from constructor`() { + // Arrange + val currency = createCurrency(rawCurrencyId = "btc", symbol = "BTC") + val currencies = listOf(createStatus(currency, loadedValue(BigDecimal("10")))) + val newWalletListUM = WalletListUM(items = persistentListOf()) + val transformer = SetPortfolioReviewTransformer( + walletListUM = newWalletListUM, + currencies = currencies, + totalFiatBalance = BigDecimal("10"), + appCurrency = appCurrency, + expandedAssetIds = emptySet(), + expandClick = {}, + onPeriodClick = {}, + ) + + // Act + val result = transformer.transform(loadingState()) + + // Assert + assertThat(result.walletListUM).isSameInstanceAs(newWalletListUM) + } + } + + private fun createTransformer( + currencies: List, + totalFiatBalance: BigDecimal, + expandedAssetIds: Set = emptySet(), + ) = SetPortfolioReviewTransformer( + walletListUM = walletListUM, + currencies = currencies, + totalFiatBalance = totalFiatBalance, + appCurrency = appCurrency, + expandedAssetIds = expandedAssetIds, + expandClick = {}, + onPeriodClick = {}, + ) + + private fun loadingState(): ForYouUM = ForYouUM( + walletListUM = walletListUM, + portfolioReviewUM = PortfolioReviewUM.Loading(tokenList = persistentListOf()), + ) + + private fun createStatus(currency: CryptoCurrency, value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus( + currency = currency, + value = value, + ) + + private fun loadedValue(fiatAmount: BigDecimal): CryptoCurrencyStatus.Loaded = mockk { + every { amount } returns BigDecimal.ONE + every { this@mockk.fiatAmount } returns fiatAmount + every { isError } returns false + } + + private fun createCurrency(rawCurrencyId: String, symbol: String): CryptoCurrency.Coin { + val network: Network = mockk { + every { name } returns "Network" + every { isTestnet } returns false + every { id } returns mockk { every { rawId } returns Network.RawID(rawCurrencyId) } + } + val currencyId: CryptoCurrency.ID = mockk { + every { value } returns "coin-$rawCurrencyId" + every { this@mockk.rawCurrencyId } returns CryptoCurrency.RawID(rawCurrencyId) + } + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.symbol } returns symbol + every { this@mockk.network } returns network + every { this@mockk.decimals } returns 8 + every { isCustom } returns false + every { iconUrl } returns null + } + } +} \ No newline at end of file