diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt index 1846999f3d..51abc977a6 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt @@ -16,6 +16,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull /** * Use case to retrieve the status of a specific cryptocurrency associated with an account. @@ -35,11 +37,8 @@ class GetAccountCurrencyStatusUseCase( * @param currency the cryptocurrency for which the status is to be retrieved. * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. */ - suspend operator fun invoke( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Option { - return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + suspend fun invokeSync(userWalletId: UserWalletId, currency: CryptoCurrency): Option { + return invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) } /** @@ -51,7 +50,7 @@ class GetAccountCurrencyStatusUseCase( * @param network the network associated with the cryptocurrency, can be null. * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. */ - suspend operator fun invoke( + suspend fun invokeSync( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network?, @@ -60,7 +59,48 @@ class GetAccountCurrencyStatusUseCase( params = SingleAccountStatusListProducer.Params(userWalletId), ) ?: return none() - return accountStatusList.getExpectedAccountStatuses(network) + return accountStatusList + .toAccountCryptoCurrencyStatus(currencyId, network) + .toOption() + } + + /** + * Retrieves the status of a specific cryptocurrency for a given user wallet as a [Flow]. + * + * @param userWalletId The ID of the user wallet. + * @param currency The cryptocurrency for which the status is to be retrieved. + * @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found. + */ + operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Flow { + return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + } + + /** + * Retrieves the status of a specific cryptocurrency by its ID for a given user wallet and network as a [Flow]. + * + * @param userWalletId The ID of the user wallet. + * @param currencyId The ID of the cryptocurrency. + * @param network The network associated with the cryptocurrency, can be null. + * @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found. + */ + operator fun invoke( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + network: Network?, + ): Flow { + return singleAccountStatusListSupplier( + params = SingleAccountStatusListProducer.Params(userWalletId), + ) + .mapNotNull { accountStatusList -> + accountStatusList.toAccountCryptoCurrencyStatus(currencyId, network) + } + } + + private fun AccountStatusList.toAccountCryptoCurrencyStatus( + currencyId: CryptoCurrency.ID, + network: Network?, + ): AccountCryptoCurrencyStatus? { + return getExpectedAccountStatuses(network) .asSequence() .filterIsInstance() .mapNotNull { accountStatus -> @@ -70,7 +110,6 @@ class GetAccountCurrencyStatusUseCase( AccountCryptoCurrencyStatus(account = accountStatus.account, status = status) } .firstOrNull() - .toOption() } /** diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt index b8614cc704..7adf3a3264 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt @@ -1,8 +1,10 @@ package com.tangem.domain.account.status.usecase +import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.assertNone import com.tangem.common.test.utils.assertSome +import com.tangem.common.test.utils.getEmittedValues import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer @@ -18,8 +20,11 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf 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 @@ -47,111 +52,238 @@ class GetAccountCurrencyStatusUseCaseTest { clearMocks(supplier) } - @Test - fun `invoke returns None when supplier returns null`() = runTest { - // Arrange - coEvery { supplier.getSyncOrNull(supplierParams) } returns null + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class InvokeSync { - // Act - val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + @Test + fun `invokeSync returns None when supplier returns null`() = runTest { + // Arrange + coEvery { supplier.getSyncOrNull(supplierParams) } returns null - // Assert - assertNone(actual) - coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + // Act + val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invokeSync returns None when AccountList does not contain required currency id`() = runTest { + // Arrange + val accountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invokeSync returns Some if network is not null`() = runTest { + // Arrange + val mainAccountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val account = mockk(relaxed = true) { + every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk()) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase.invokeSync( + userWalletId = userWalletId, + currencyId = currency.id, + network = currency.network, + ) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invokeSync returns Some if network is null`() = runTest { + // Arrange + val account = mockk(relaxed = true) { + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } } - @Test - fun `invoke returns None when AccountList does not contain required currency id`() = runTest { - // Arrange - val accountStatus = AccountStatus.CryptoPortfolio( - account = Account.CryptoPortfolio.createMainAccount(userWalletId), - tokenList = TokenList.Empty, - priceChangeLce = lceLoading(), - ) + @Suppress("UnusedFlow") + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Invoke { - val accountStatusList = mockk(relaxed = true) { - every { this@mockk.accountStatuses } returns setOf(accountStatus) + @Test + fun `invoke returns empty flow when supplier returns empty flow`() = runTest { + // Arrange + coEvery { supplier(supplierParams) } returns emptyFlow() + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + .let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + coVerifyOrder { supplier(supplierParams) } } - coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + @Test + fun `invoke returns empty flow when AccountList does not contain required currency id`() = runTest { + // Arrange + val accountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) - // Act - val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } - // Assert - assertNone(actual) - coVerifyOrder { supplier.getSyncOrNull(supplierParams) } - } + coEvery { supplier(supplierParams) } returns flowOf(accountStatusList) - @Test - fun `invoke returns Some if network is not null`() = runTest { - // Arrange - val mainAccountStatus = AccountStatus.CryptoPortfolio( - account = Account.CryptoPortfolio.createMainAccount(userWalletId), - tokenList = TokenList.Empty, - priceChangeLce = lceLoading(), - ) + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + .let(::getEmittedValues) - val account = mockk(relaxed = true) { - every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! - every { this@mockk.cryptoCurrencies } returns setOf(currency) - } - val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) - val accountStatus = AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenList.Ungrouped( - totalFiatBalance = TotalFiatBalance.Loading, - sortedBy = TokensSortType.NONE, - currencies = listOf(currencyStatus), - ), - priceChangeLce = lceLoading(), - ) - - val accountStatusList = mockk(relaxed = true) { - every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk()) + // Assert + Truth.assertThat(actual).isEmpty() + coVerifyOrder { supplier(supplierParams) } } - coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + @Test + fun `invoke returns data if network is not null`() = runTest { + // Arrange + val mainAccountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) - // Act - val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + val account = mockk(relaxed = true) { + every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) - // Assert - val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) - assertSome(actual, expected) + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk()) + } - coVerifyOrder { supplier.getSyncOrNull(supplierParams) } - } + coEvery { supplier(supplierParams) } returns flowOf(accountStatusList) - @Test - fun `invoke returns Some if network is null`() = runTest { - // Arrange - val account = mockk(relaxed = true) { - every { this@mockk.cryptoCurrencies } returns setOf(currency) - } - val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) - val accountStatus = AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenList.Ungrouped( - totalFiatBalance = TotalFiatBalance.Loading, - sortedBy = TokensSortType.NONE, - currencies = listOf(currencyStatus), - ), - priceChangeLce = lceLoading(), - ) + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + .let(::getEmittedValues) - val accountStatusList = mockk(relaxed = true) { - every { this@mockk.accountStatuses } returns setOf(accountStatus) + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + Truth.assertThat(actual).containsExactly(expected) + + coVerifyOrder { supplier(supplierParams) } } - coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + @Test + fun `invoke returns data if network is null`() = runTest { + // Arrange + val account = mockk(relaxed = true) { + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) - // Act - val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } - // Assert - val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) - assertSome(actual, expected) - coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + coEvery { supplier(supplierParams) } returns flowOf(accountStatusList) + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + .let(::getEmittedValues) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + Truth.assertThat(actual).containsExactly(expected) + coVerifyOrder { supplier(supplierParams) } + } } } \ No newline at end of file