From 1b33e6b02ee59aed787e2388d58504d34102fa11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:33:20 +0400 Subject: [PATCH] Updated on 2026-08-14 --- data/networks/build.gradle.kts | 7 +- .../repository/DefaultNetworksRepository.kt | 88 ++++++ .../DefaultNetworksRepositoryTest.kt | 268 ++++++++++++++++++ .../networks/repository/NetworksRepository.kt | 24 ++ gradle/dependencies.toml | 3 + 5 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index 1dc873674c..510981e800 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.data.networks" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { // region Project - Core implementation(projects.core.datasource) @@ -52,7 +56,8 @@ dependencies { // region Tests testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt new file mode 100644 index 0000000000..97bb6128ac --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -0,0 +1,88 @@ +package com.tangem.data.networks.repository + +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.storeStatus +import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Default implementation of [NetworksRepository] + * + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultNetworksRepository( + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val walletManagersFacade: WalletManagersFacade, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, +) : NetworksRepository { + + override suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) { + withContext(dispatchers.default) { + val currencies = runCatching { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } + .getOrElse { + Timber.e(it, "Unable to create wallet currencies") + return@withContext + } + + fetchPendingTransactions(userWalletId = userWalletId, network = network, currencies = currencies) + } + } + + override suspend fun getNetworkAddresses( + userWalletId: UserWalletId, + network: Network, + ): List { + return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + .getOrElse { + Timber.e(it, "Unable to create wallet currencies") + return emptyList() + } + .map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = getDefaultAddress(userWalletId, network), + ) + } + } + + private suspend fun fetchPendingTransactions( + userWalletId: UserWalletId, + network: Network, + currencies: List, + ) { + val result = withContext(dispatchers.io) { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } + + val networkStatus = NetworkStatusFactory.create( + network = network, + updatingResult = result, + addedCurrencies = currencies.toSet(), + ) + + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = networkStatus) + } + + private suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String { + return withContext(dispatchers.io) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network).orEmpty() + } + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt b/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt new file mode 100644 index 0000000000..1facb5017b --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt @@ -0,0 +1,268 @@ +package com.tangem.data.networks.repository + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.walletmanager.MockUpdateWalletManagerResultFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.storeStatus +import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +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 + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultNetworksRepositoryTest { + + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + + private val repository: DefaultNetworksRepository = DefaultNetworksRepository( + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(cardCryptoCurrencyFactory, walletManagersFacade, networksStatusesStore) + } + + @Nested + inner class FetchPendingTransactions { + + @Test + fun `walletManagersFacade returns Verified`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createVerified() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns NoAccount`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createNoAccount() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns MissedDerivation`() = runTest { + // Arrange + val result = UpdateWalletManagerResult.MissedDerivation + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns Unreachable`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createUnreachableWithAddress() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `cardCryptoCurrencyFactory throws exception`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } throws IllegalStateException() + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerify { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } + + coVerify(inverse = true) { + walletManagersFacade.updatePendingTransactions(userWalletId = any(), network = any()) + } + } + } + + @Nested + inner class GetNetworkAddresses { + + @Test + fun `cardCryptoCurrencyFactory create throws exception`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } throws IllegalStateException() + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + coVerify(inverse = true) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } + } + + @Test + fun `cardCryptoCurrencyFactory create returns empty list`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns emptyList() + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + coVerify(inverse = true) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } + } + + @Test + fun `cardCryptoCurrencyFactory create returns not empty list`() = runTest { + // Arrange + val currencies = listOf(cryptoCurrency) + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns currencies + + coEvery { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = cryptoCurrency.network) + } returns "address" + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = listOf( + CryptoCurrencyAddress(cryptoCurrency = cryptoCurrency, address = "address"), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = cryptoCurrency.network) + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = cryptoCurrency.network) + } + } + } + + private companion object { + + val userWalletId = UserWalletId("011") + val cryptoCurrency = MockCryptoCurrencyFactory().ethereum + val network = MockCryptoCurrencyFactory().ethereum.network + + val updateWalletManagerResultFactory = MockUpdateWalletManagerResultFactory() + + fun UpdateWalletManagerResult.toNetworkStatus(): NetworkStatus { + return NetworkStatusFactory.create( + network = network, + updatingResult = this, + addedCurrencies = setOf(cryptoCurrency), + ) + } + } +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt new file mode 100644 index 0000000000..c828d3f38a --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.networks.repository + +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Repository for working with pending transactions + * +[REDACTED_AUTHOR] + */ +interface NetworksRepository { + + /** Fetches pending transactions for given [network] in selected [userWalletId] */ + suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) + + /** + * Returns addresses and crypto currency + * + * @param userWalletId the unique identifier of the user wallet + * @param network network + */ + suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 2353d61b82..ef9fae7586 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -100,6 +100,7 @@ detekt = "1.22.0" espresso = "3.5.1" espresso-intents = "3.5.1" junit = "4.13.2" +junit5 = "5.8.2" junitAndroidExt = "1.1.5" mockk = "1.13.4" turbine = "1.2.0" @@ -197,6 +198,8 @@ test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ver test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso-intents" } test-junit = { module = "junit:junit", version.ref = "junit" } +test-junit5 = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } +test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } test-truth = { module = "com.google.truth:truth", version.ref = "truth" } test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }