From c38c185a2d09679006e82e311de3b0555ef30101 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 22:06:02 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../test/datastore/MockStateDataStore.kt | 16 ++ .../network/MockNetworkStatusFactory.kt | 58 +++++ .../converter/NetworkAddressConverter.kt | 2 +- .../converter/NetworkAmountsConverter.kt | 2 +- .../NetworkDerivationPathConverter.kt | 2 +- .../NetworkStatusDataModelConverter.kt | 2 +- .../local/network/entity/NetworkStatusDM.kt | 2 +- data/networks/.gitignore | 1 + data/networks/build.gradle.kts | 43 ++++ .../SimpleNetworkStatusConverter.kt | 50 ++++ .../networks/models/SimpleNetworkStatus.kt | 27 +++ .../store/DefaultNetworksStatusesStoreV2.kt | 133 +++++++++++ .../networks/store/NetworksStatusesStoreV2.kt | 23 ++ .../tangem/data/networks/NetworkStatusExt.kt | 17 ++ .../NetworkStatusesStoreUpdateMethodsTest.kt | 214 ++++++++++++++++++ .../NetworksStatusesStoreGetMethodTest.kt | 83 +++++++ ...NetworksStatusesStoreInitializationTest.kt | 83 +++++++ .../store/StoreActualNetworkStatusTest.kt | 104 +++++++++ settings.gradle.kts | 1 + 19 files changed, 858 insertions(+), 5 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt create mode 100644 common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt create mode 100644 data/networks/.gitignore create mode 100644 data/networks/build.gradle.kts create mode 100644 data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt b/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt new file mode 100644 index 0000000000..2fc75fe7f1 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt @@ -0,0 +1,16 @@ +package com.tangem.common.test.datastore + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.updateAndGet + +class MockStateDataStore(default: T) : DataStore { + + private val _data = MutableStateFlow(value = default) + override val data: Flow = _data + + override suspend fun updateData(transform: suspend (t: T) -> T): T { + return _data.updateAndGet { transform(it) } + } +} \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt new file mode 100644 index 0000000000..34153765ce --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.common.test.domain.network + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +object MockNetworkStatusFactory { + + private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network + + fun createVerified(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus( + network = network, + value = NetworkStatus.Verified( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amounts = mapOf(), + pendingTransactions = mapOf(), + source = StatusSource.ACTUAL, + ), + ) + } + + fun createNoAccount(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus( + network = network, + value = NetworkStatus.NoAccount( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "", + source = StatusSource.ACTUAL, + ), + ) + } + + fun createMissedDerivation(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus(network = network, value = NetworkStatus.MissedDerivation) + } + + fun createUnreachable(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus(network = network, value = NetworkStatus.Unreachable(address = null)) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt index e0c0192c84..5d3ba03610 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt @@ -10,7 +10,7 @@ import timber.log.Timber * [REDACTED_AUTHOR] */ -internal class NetworkAddressConverter( +class NetworkAddressConverter( private val selectedAddress: String, ) : TwoWayConverter, NetworkAddress> { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt index cea7a8a4b7..e3bac6232a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt @@ -14,7 +14,7 @@ private typealias AmountsDomainModel = Map { +object NetworkAmountsConverter : TwoWayConverter { override fun convert(value: AmountsDataModel): AmountsDomainModel { return value diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt index 92925a866f..1cffbc73f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt @@ -10,7 +10,7 @@ import com.tangem.utils.converter.TwoWayConverter * [REDACTED_AUTHOR] */ -internal object NetworkDerivationPathConverter : +object NetworkDerivationPathConverter : TwoWayConverter { override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt index 34dc695aba..55ab2be060 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt @@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -internal object NetworkStatusDataModelConverter : Converter { +object NetworkStatusDataModelConverter : Converter { override fun convert(value: NetworkStatus): NetworkStatusDM? { return when (val status = value.value) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 0ada82055f..40f26e9e08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.model.Network import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import java.math.BigDecimal -internal sealed interface NetworkStatusDM { +sealed interface NetworkStatusDM { val networkId: Network.ID val derivationPath: DerivationPath diff --git a/data/networks/.gitignore b/data/networks/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/networks/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts new file mode 100644 index 0000000000..f74ca832ca --- /dev/null +++ b/data/networks/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.networks" +} + +dependencies { + implementation(projects.libs.blockchainSdk) + + implementation(projects.core.datasource) + implementation(projects.core.utils) + + implementation(projects.data.common) + implementation(projects.data.tokens) + + implementation(projects.domain.core) + implementation(projects.domain.demo) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + implementation(deps.androidx.datastore) + implementation(deps.moshi) + implementation(deps.timber) + + implementation(tangemDeps.blockchain) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(tangemDeps.card.core) + testImplementation(projects.common.test) +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt new file mode 100644 index 0000000000..5bfe00747d --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.data.networks.converters + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.network.converter.NetworkAddressConverter +import com.tangem.datasource.local.network.converter.NetworkAmountsConverter +import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.converter.Converter + +/** + * Converter from [NetworkStatusDM] to [SimpleNetworkStatus] + * +[REDACTED_AUTHOR] + */ +internal object SimpleNetworkStatusConverter : Converter { + + override fun convert(value: NetworkStatusDM): SimpleNetworkStatus { + val address = NetworkAddressConverter(selectedAddress = value.selectedAddress) + .convert(value = value.availableAddresses) + + val status = when (value) { + is NetworkStatusDM.Verified -> { + NetworkStatus.Verified( + address = address, + amounts = NetworkAmountsConverter.convert(value = value.amounts), + pendingTransactions = mapOf(), + source = StatusSource.CACHE, + ) + } + is NetworkStatusDM.NoAccount -> { + NetworkStatus.NoAccount( + address = address, + amountToCreateAccount = value.amountToCreateAccount, + errorMessage = value.errorMessage, + source = StatusSource.CACHE, + ) + } + } + + return SimpleNetworkStatus( + id = SimpleNetworkStatus.Id( + networkId = value.networkId, + derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath), + ), + value = status, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt b/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt new file mode 100644 index 0000000000..6ee9a975b9 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt @@ -0,0 +1,27 @@ +package com.tangem.data.networks.models + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus + +/** + * Simplified model of network status. Like [NetworkStatus] but without metadata. + * + * @property id identifier + * @property value value + */ +internal data class SimpleNetworkStatus(val id: Id, val value: NetworkStatus.Value) { + + /** + * Identifier of [SimpleNetworkStatus] + * + * @property networkId network id + * @property derivationPath derivation path + */ + data class Id(val networkId: Network.ID, val derivationPath: Network.DerivationPath) { + + constructor(network: Network) : this( + networkId = network.id, + derivationPath = network.derivationPath, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt new file mode 100644 index 0000000000..776b446e54 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -0,0 +1,133 @@ +package com.tangem.data.networks.store + +import androidx.datastore.core.DataStore +import com.tangem.data.networks.converters.SimpleNetworkStatusConverter +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +internal typealias WalletIdWithSimpleStatus = Map> +internal typealias WalletIdWithStatusDM = Map> + +/** + * Default implementation of [NetworksStatusesStoreV2] + * + * @property runtimeStore runtime store + * @property persistenceDataStore persistence store + * @param dispatchers dispatchers + */ +internal class DefaultNetworksStatusesStoreV2( + private val runtimeStore: RuntimeSharedStore, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) : NetworksStatusesStoreV2 { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + + runtimeStore.store( + value = cachedStatuses.mapValues { (_, statuses) -> + SimpleNetworkStatusConverter.convertSet(statuses) + }, + ) + } + } + + override fun get(userWalletId: UserWalletId): Flow> { + return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + } + + override suspend fun refresh(userWalletId: UserWalletId, network: Network) { + updateStatusSourceInRuntime(userWalletId = userWalletId, network = network, source = StatusSource.CACHE) + } + + override suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) { + if (value.value.source != StatusSource.ACTUAL) { + error("Method storeActual can be called only with StatusSource.ACTUAL") + } + + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, status = value) } + launch { storeInPersistence(userWalletId = userWalletId, status = value) } + } + } + + override suspend fun storeError(userWalletId: UserWalletId, network: Network) { + updateStatusSourceInRuntime(userWalletId = userWalletId, network = network, source = StatusSource.ONLY_CACHE) + } + + private suspend fun updateStatusSourceInRuntime( + userWalletId: UserWalletId, + network: Network, + source: StatusSource, + ) { + val simpleStatusId = SimpleNetworkStatus.Id(network) + + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val status = this[userWalletId.stringValue].orEmpty() + .firstOrNull { it.id == simpleStatusId } + ?: SimpleNetworkStatus( + id = simpleStatusId, + value = NetworkStatus.Unreachable(address = null), + ) + + val updatedStatus = status.copy( + value = status.value.copySealed(source = source), + ) + + val updatedStatuses = this[userWalletId.stringValue].orEmpty() + .addOrReplace(updatedStatus) { it.id == simpleStatusId } + + put(key = userWalletId.stringValue, value = updatedStatuses) + } + } + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: NetworkStatus) { + val simpleStatusId = SimpleNetworkStatus.Id(network = status.network) + + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val simpleStatus = SimpleNetworkStatus(id = simpleStatusId, value = status.value) + + val updatedStatuses = this[userWalletId.stringValue].orEmpty() + .addOrReplace(simpleStatus) { it.id == simpleStatusId } + + put(key = userWalletId.stringValue, value = updatedStatuses) + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: NetworkStatus) { + // Converter will return null if the network status is not supported + val statusDM = NetworkStatusDataModelConverter.convert(value = status) ?: return + + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + val updatedValues = this[userWalletId.stringValue].orEmpty().addOrReplace(statusDM) { + it.networkId == statusDM.networkId && it.derivationPath == statusDM.derivationPath + } + + this[userWalletId.stringValue] = updatedValues + } + } + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt new file mode 100644 index 0000000000..356df1dbf1 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -0,0 +1,23 @@ +package com.tangem.data.networks.store + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** Store of [NetworkStatus]'es set */ +internal interface NetworksStatusesStoreV2 { + + /** Get statuses by [userWalletId] */ + fun get(userWalletId: UserWalletId): Flow> + + /** Refresh status of [network] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, network: Network) + + /** Store actual [NetworkStatus] by [userWalletId] */ + suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) + + /** Store error by [userWalletId] and [network] */ + suspend fun storeError(userWalletId: UserWalletId, network: Network) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt new file mode 100644 index 0000000000..1b19c1bc0f --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt @@ -0,0 +1,17 @@ +package com.tangem.data.networks + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.NetworkStatus + +internal fun NetworkStatus.toSimple(): SimpleNetworkStatus { + return SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network = network), + value = value, + ) +} + +internal fun NetworkStatus.toDataModel(): NetworkStatusDM? { + return NetworkStatusDataModelConverter.convert(value = this) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt new file mode 100644 index 0000000000..d2e5f3c533 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt @@ -0,0 +1,214 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworkStatusesStoreUpdateMethodsTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `refresh if runtime store is empty`() = runTest { + store.refresh(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified().toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.refresh(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store actual with any status sources`() = runTest { + val expectedErrorMessage = "Method storeActual can be called only with StatusSource.ACTUAL" + + // #1: StatusSource.ACTUAL + val status = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ACTUAL), + ) + + val actual = runCatching { store.storeActual(userWalletId, status) } + + Truth.assertThat(actual.isSuccess).isTrue() + + // #2: StatusSource.CACHE + val cacheStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.CACHE), + ) + + val cacheActual = runCatching { store.storeActual(userWalletId, cacheStatus) } + + Truth.assertThat(cacheActual.isFailure).isTrue() + Truth.assertThat(cacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + Truth.assertThat(cacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) + + // #3: StatusSource.ONLY_CACHE + val onlyCacheStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ONLY_CACHE), + ) + + val onlyCacheActual = runCatching { store.storeActual(userWalletId, onlyCacheStatus) } + + Truth.assertThat(onlyCacheActual.isFailure).isTrue() + Truth.assertThat(onlyCacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + Truth.assertThat(onlyCacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) + } + + @Test + fun `store actual if runtime and cache stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ONLY_CACHE), + ) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) + } + } + + val newStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ACTUAL), + ) + + store.storeActual(userWalletId, newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store actual unreachable status if cache store contains verified status`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) + } + } + + val newStatus = MockNetworkStatusFactory.createUnreachable() + + store.storeActual(userWalletId, newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(prevStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store error if runtime store is empty`() = runTest { + store.storeError(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store error if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified().toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.storeError(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt new file mode 100644 index 0000000000..df73b079bb --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt @@ -0,0 +1,83 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworksStatusesStoreGetMethodTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test get if runtime store is empty`() = runTest { + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `test get if runtime store is not empty`() = runTest { + val status = MockNetworkStatusFactory.createVerified() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status.toSimple())), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(setOf(status.toSimple()))) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt new file mode 100644 index 0000000000..08314ee68e --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt @@ -0,0 +1,83 @@ +package com.tangem.data.networks.store + +import androidx.datastore.core.DataStore +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworksStatusesStoreInitializationTest { + + @Test + fun `test initialization if cache store is empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, // local mock + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) + } + + @Test + fun `test initialization if cache store contains empty map`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `test initialization if cache store is not empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + val status = MockNetworkStatusFactory.createVerified() + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(status.toDataModel()!!)) + } + } + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val expectedStatus = status.toSimple().copy(value = status.value.copySealed(source = StatusSource.CACHE)) + val expected = mapOf(userWalletId.stringValue to setOf(expectedStatus)) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected) + } + + private companion object { + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt new file mode 100644 index 0000000000..0824241177 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt @@ -0,0 +1,104 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class StoreActualNetworkStatusTest(private val model: Model) { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `store actual`() = runTest { + store.storeActual(userWalletId = userWalletId, value = model.status) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) + + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) + } + + data class Model( + val status: NetworkStatus, + val runtimeExpected: Map>, + val persistenceExpected: WalletIdWithStatusDM, + ) + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf( + Model( + status = MockNetworkStatusFactory.createVerified(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createVerified().toSimple(), + ), + ), + persistenceExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createVerified().toDataModel()!!, + ), + ), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createNoAccount().toSimple(), + ), + ), + persistenceExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createNoAccount().toDataModel()!!, + ), + ), + ), + Model( + status = MockNetworkStatusFactory.createMissedDerivation(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createMissedDerivation().toSimple(), + ), + ), + persistenceExpected = mapOf(), + ), + Model( + status = MockNetworkStatusFactory.createUnreachable(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createUnreachable().toSimple(), + ), + ), + persistenceExpected = mapOf(), + ), + ) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b85da198aa..a63804ffe8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -309,6 +309,7 @@ include(":data:staking") include(":data:wallet-connect") include(":data:markets") include(":data:manage-tokens") +include(":data:networks") include(":data:nft") include(":data:onramp") // endregion Data modules \ No newline at end of file