From 83d291d7d0cf385d5afa90ca1a14214f37e1ec8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 5 Jan 2025 15:42:16 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultBlockchainSDKFactory.kt | 37 +--- .../di/BlockchainSDKFactoryModule.kt | 19 +- .../BlockchainProvidersResponseLoader.kt | 127 ++--------- .../BlockchainProvidersResponseMerger.kt | 106 +++++++++ .../BlockchainProvidersTypesManager.kt | 11 + .../ProdBlockchainProvidersTypesManager.kt | 39 ++++ .../BlockchainProvidersResponseLoaderTest.kt | 201 ++--------------- .../BlockchainProvidersResponseMergerTest.kt | 205 ++++++++++++++++++ 8 files changed, 422 insertions(+), 323 deletions(-) create mode 100644 libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt create mode 100644 libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt create mode 100644 libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt create mode 100644 libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 69a5bdaa75..f4ca59e975 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -1,9 +1,7 @@ package com.tangem.blockchainsdk import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter -import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore -import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -11,24 +9,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber internal typealias BlockchainProvidersResponse = Map> /** * Implementation of Blockchain SDK components factory * - * @property blockchainProvidersResponseLoader blockchain providers response loader - * @property environmentConfigStorage environment config storage - * @property blockchainProviderTypesStore blockchain provider types store - * @property walletManagerFactoryCreator wallet manager factory creator + * @property blockchainProvidersTypesManager blockchain providers types manager + * @property environmentConfigStorage environment config storage + * @property walletManagerFactoryCreator wallet manager factory creator + * @param dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultBlockchainSDKFactory( - private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager, private val environmentConfigStorage: EnvironmentConfigStorage, - private val blockchainProviderTypesStore: BlockchainProviderTypesStore, private val walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKFactory { @@ -39,7 +35,7 @@ internal class DefaultBlockchainSDKFactory( override suspend fun init() { coroutineScope { - updateBlockchainProviderTypes() + launch { blockchainProvidersTypesManager.update() } } } @@ -48,28 +44,11 @@ internal class DefaultBlockchainSDKFactory( private fun createWalletManagerFactory(): Flow { return combine( flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig }, - flow2 = blockchainProviderTypesStore.get(), + flow2 = blockchainProvidersTypesManager.get(), // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) // don't use Lazily because some features (WC) require initialized factory on app started .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } - - private fun CoroutineScope.updateBlockchainProviderTypes() { - launch { - val response = blockchainProvidersResponseLoader.load() - - if (response == null) { - Timber.e("Error loading BlockchainProviderTypes") - return@launch - } - - Timber.i("Update BlockchainProviderTypes") - - blockchainProviderTypesStore.store( - value = BlockchainProviderTypesConverter.convert(response), - ) - } - } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 4511516692..3d1145c79d 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.WalletManagerFactoryCreator import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles -import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore -import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager +import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -27,21 +27,28 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainSDKFactory( - blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + blockchainProvidersTypesManager: BlockchainProvidersTypesManager, environmentConfigStorage: EnvironmentConfigStorage, walletManagerFactoryCreator: WalletManagerFactoryCreator, - blockchainProviderTypesStore: BlockchainProviderTypesStore, dispatchers: CoroutineDispatcherProvider, ): BlockchainSDKFactory { return DefaultBlockchainSDKFactory( - blockchainProvidersResponseLoader = blockchainProvidersResponseLoader, + blockchainProvidersTypesManager = blockchainProvidersTypesManager, environmentConfigStorage = environmentConfigStorage, - blockchainProviderTypesStore = blockchainProviderTypesStore, walletManagerFactoryCreator = walletManagerFactoryCreator, dispatchers = dispatchers, ) } + @Provides + @Singleton + fun provideBlockchainProvidersTypesManager( + prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager, + ): BlockchainProvidersTypesManager { + // In the future, we will use different types of providers for different environments + return prodBlockchainProvidersTypesManager + } + @Provides @Singleton fun provideWalletManagerFactoryCreator( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index 6a3fc7ffc4..4336fc4db4 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -1,12 +1,8 @@ package com.tangem.blockchainsdk.providers -import androidx.core.util.PatternsCompat -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse -import com.tangem.blockchainsdk.utils.createPrivateProviderType import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage -import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import timber.log.Timber @@ -16,7 +12,7 @@ import javax.inject.Singleton /** * Loader of [BlockchainProvidersResponse] * - * @property tangemTechServiceApi tangem tech api + * @property tangemTechApi tangem tech api * @property blockchainProvidersStorage blockchain providers storage * @property dispatchers dispatchers * @@ -24,120 +20,33 @@ import javax.inject.Singleton */ @Singleton internal class BlockchainProvidersResponseLoader @Inject constructor( - private val tangemTechServiceApi: TangemTechApi, + private val tangemTechApi: TangemTechApi, private val blockchainProvidersStorage: BlockchainProvidersStorage, private val dispatchers: CoroutineDispatcherProvider, ) { - private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) - /** Load [BlockchainProvidersResponse] */ suspend fun load(): BlockchainProvidersResponse? { val localResponse = loadLocal().ifEmpty { return null } - return runCatching(dispatcher = dispatchers.io, block = ::loadRemote) - .fold( - onSuccess = { remoteResponse -> mergeResponses(local = localResponse, remote = remoteResponse) }, - onFailure = { - Timber.e(it, "Failed to load blockchain provider types from backend") - localResponse - }, - ) + return loadRemote().fold( + onSuccess = { remoteResponse -> + BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteResponse, + ) + }, + onFailure = { + Timber.e(it, "Failed to load blockchain provider types from backend") + localResponse + }, + ) } private suspend fun loadLocal(): BlockchainProvidersResponse = blockchainProvidersStorage.getConfigSync() - private suspend fun loadRemote() = tangemTechServiceApi.getBlockchainProviders() - - /** Merge blockchains with non-empty providers [remote] from remote with blockchains from local [local] */ - private fun mergeResponses( - local: BlockchainProvidersResponse, - remote: BlockchainProvidersResponse, - ): BlockchainProvidersResponse { - /* - * Example: - * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) - * val local = mapOf("a" to 11, "e" to 4, "f" to 5) - * - * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } - */ - val remoteWithoutInvalidProviders = remote - .mapValues { - it.value - .filterUnsupportedProviders() - .filterInvalidProviders() - } - .filterValues { it.isNotEmpty() } - - val result = local + remoteWithoutInvalidProviders - - if (result != remote) { - val missingBlockchains = result.keys - remote.keys - val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys - - recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) - } - - return result.guaranteeUrlsEndWithSlash() - } - - private fun List.filterUnsupportedProviders() = filter { - val isSupportedType = it !is ProviderModel.UnsupportedType - - val isSupportedPrivateType = if (it is ProviderModel.Private) { - createPrivateProviderType(it.name) != null - } else { - true - } - - isSupportedType && isSupportedPrivateType - } - - private fun List.filterInvalidProviders() = mapNotNull { provider -> - if (provider is ProviderModel.Public) { - if (isValidUrl(provider.url)) provider else null - } else { - provider - } - } - - private fun isValidUrl(url: String): Boolean { - val forbiddenScheme = forbiddenSchemes.firstOrNull { url.startsWith(prefix = it) } - val inputUrl = if (forbiddenScheme != null) url.substringAfter(forbiddenScheme) else url - - return PatternsCompat.WEB_URL.matcher(inputUrl).matches() - } - - private fun recordException(missingBlockchains: Set) { - val exception = IllegalStateException( - "Remote config does not contain required blockchains or providers information: " + - missingBlockchains.joinToString(), - ) - - Timber.e(exception) - - firebaseCrashlytics.recordException(exception) - } - - /* - * Example: - * https://qwe.com --> https://qwe.com/ - */ - private fun BlockchainProvidersResponse.guaranteeUrlsEndWithSlash(): BlockchainProvidersResponse { - return mapValues { - it.value.map { provider -> provider.addSlashIfAbsent() } - } - } - - private fun ProviderModel.addSlashIfAbsent(): ProviderModel { - return if (this is ProviderModel.Public && url.last() != '/') { - copy(url = "$url/") - } else { - this - } - } - - private companion object { - val forbiddenSchemes = listOf("wss://") - } + private suspend fun loadRemote() = runCatching( + dispatcher = dispatchers.io, + block = tangemTechApi::getBlockchainProviders, + ) } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt new file mode 100644 index 0000000000..8afaacc12d --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -0,0 +1,106 @@ +package com.tangem.blockchainsdk.providers + +import androidx.core.util.PatternsCompat +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.utils.createPrivateProviderType +import com.tangem.datasource.local.config.providers.models.ProviderModel +import timber.log.Timber + +/** + * Merger of [BlockchainProvidersResponse] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainProvidersResponseMerger { + + private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) + + private val forbiddenSchemes = listOf("wss://") + + /** + * Merge blockchains with non-empty providers from [remote] with blockchains from [local] + * + * Example: + * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) + * val local = mapOf("a" to 11, "e" to 4, "f" to 5) + * + * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } + */ + fun merge(local: BlockchainProvidersResponse, remote: BlockchainProvidersResponse): BlockchainProvidersResponse { + val remoteWithoutInvalidProviders = remote + .mapValues { + it.value + .filterUnsupportedProviders() + .filterInvalidProviders() + } + .filterValues { it.isNotEmpty() } + + val result = local + remoteWithoutInvalidProviders + + if (result != remote) { + val missingBlockchains = result.keys - remote.keys + val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys + + recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + } + + return result.guaranteeUrlsEndWithSlash() + } + + private fun List.filterUnsupportedProviders() = filter { + val isSupportedType = it !is ProviderModel.UnsupportedType + + val isSupportedPrivateType = if (it is ProviderModel.Private) { + createPrivateProviderType(it.name) != null + } else { + true + } + + isSupportedType && isSupportedPrivateType + } + + private fun List.filterInvalidProviders() = mapNotNull { provider -> + if (provider is ProviderModel.Public) { + if (isValidUrl(provider.url)) provider else null + } else { + provider + } + } + + private fun isValidUrl(url: String): Boolean { + val forbiddenScheme = forbiddenSchemes.firstOrNull { url.startsWith(prefix = it) } + val inputUrl = if (forbiddenScheme != null) url.substringAfter(forbiddenScheme) else url + + return PatternsCompat.WEB_URL.matcher(inputUrl).matches() + } + + private fun recordException(missingBlockchains: Set) { + val exception = IllegalStateException( + "Remote config does not contain required blockchains or providers information: " + + missingBlockchains.joinToString(), + ) + + Timber.e(exception) + + firebaseCrashlytics.recordException(exception) + } + + /* + * Example: + * https://qwe.com --> https://qwe.com/ + */ + private fun BlockchainProvidersResponse.guaranteeUrlsEndWithSlash(): BlockchainProvidersResponse { + return mapValues { + it.value.map { provider -> provider.addSlashIfAbsent() } + } + } + + private fun ProviderModel.addSlashIfAbsent(): ProviderModel { + return if (this is ProviderModel.Public && url.last() != '/') { + copy(url = "$url/") + } else { + this + } + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..af43d092c8 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt @@ -0,0 +1,11 @@ +package com.tangem.blockchainsdk.providers + +import kotlinx.coroutines.flow.StateFlow + +/** Blockchain providers types manager */ +interface BlockchainProvidersTypesManager { + + fun get(): StateFlow + + suspend fun update() +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt new file mode 100644 index 0000000000..3a62906d7b --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt @@ -0,0 +1,39 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of [BlockchainProvidersTypesManager] in PROD environment + * + * @property blockchainProvidersResponseLoader blockchain providers response loader + * @property blockchainProviderTypesStore blockchain provider types store + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class ProdBlockchainProvidersTypesManager @Inject constructor( + private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val blockchainProviderTypesStore: BlockchainProviderTypesStore, +) : BlockchainProvidersTypesManager { + + override fun get(): StateFlow = blockchainProviderTypesStore.get() + + override suspend fun update() { + val response = blockchainProvidersResponseLoader.load() + + if (response == null) { + Timber.e("Error loading BlockchainProviderTypes") + return + } + + Timber.i("Update BlockchainProviderTypes") + + blockchainProviderTypesStore.store( + value = BlockchainProviderTypesConverter.convert(response), + ) + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt index b1fbac91f3..54d99add4a 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt @@ -2,7 +2,8 @@ package com.tangem.blockchainsdk.providers import com.google.common.truth.Truth import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.localResponse +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.remoteResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.models.ProviderModel @@ -17,229 +18,71 @@ import org.junit.Test */ internal class BlockchainProvidersResponseLoaderTest { - private val tangemTechServiceApi = mockk() + private val tangemTechApi = mockk() private val blockchainProvidersStorage = mockk() private val loader = BlockchainProvidersResponseLoader( - tangemTechServiceApi = tangemTechServiceApi, + tangemTechApi = tangemTechApi, blockchainProvidersStorage = blockchainProvidersStorage, dispatchers = TestingCoroutineDispatcherProvider(), ) - private val firebaseCrashlytics = mockk() - @Before fun setup() { mockkStatic(FirebaseCrashlytics::class) + val firebaseCrashlytics = mockk() every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics + every { firebaseCrashlytics.recordException(any()) } just Runs } @Test - fun test_load_if_local_config_is_empty() = runTest { + fun test_if_local_config_is_empty() = runTest { coEvery { blockchainProvidersStorage.getConfigSync() } returns emptyMap() + val expected = null + val actual = loader.load() coVerifyOrder { blockchainProvidersStorage.getConfigSync() } - coVerify(inverse = true) { tangemTechServiceApi.getBlockchainProviders() } - - Truth.assertThat(actual).isEqualTo(null) - } - - @Test - fun test_load_if_remote_config_loading_is_loaded_failure() = runTest { - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } throws IllegalStateException("Test exception") - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(localProviders) - } - - @Test - fun test_load_if_remote_config_loading_is_loaded_successful() = runTest { - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_is_the_same_as_local() = runTest { - val remoteProviders = localProviders - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_has_empty_providers() = runTest { - val ethProvider = "ethereum" to emptyList() - val remoteProvidersWithEth = remoteProviders + ethProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_local_config_has_empty_providers() = runTest { - val localProvidersWithEmptyApt = localProviders.mapValues { if (it.key == "aptos") emptyList() else it.value } - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEmptyApt - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - } - - val expected = localProvidersWithEmptyApt + remoteProviders + coVerify(inverse = true) { tangemTechApi.getBlockchainProviders() } Truth.assertThat(actual).isEqualTo(expected) } @Test - fun test_load_if_remote_config_doesnt_contain_local_providers() = runTest { - val remoteProvidersWithoutLocal: BlockchainProvidersResponse = remoteProviders - localProviders.keys + fun test_if_remote_config_loading_is_failed() = runTest { + coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse + coEvery { tangemTechApi.getBlockchainProviders() } throws IllegalStateException("Test exception") - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithoutLocal - everyCrashlyticsRecording() just Runs + val expected = localResponse val actual = loader.load() coVerifyOrder { blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) + tangemTechApi.getBlockchainProviders() } - Truth.assertThat(actual).isEqualTo(remoteProviders) - } - - @Test - fun test_load_if_remote_config_contains_unsupported_types() = runTest { - val ethProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Private(name = "nownodes")) - val remoteProvidersWithEth = remoteProviders + ethProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProviders - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - val expected = remoteProviders + ("ethereum" to listOf(ProviderModel.Private(name = "nownodes"))) - Truth.assertThat(actual).isEqualTo(expected) } @Test - fun test_load_if_remote_config_contains_invalid_public_providers() = runTest { - val localEthProvider = "ethereum" to listOf(ProviderModel.Private(name = "nownodes")) - val localProvidersWithEth = localProviders + localEthProvider + fun test_if_remote_config_is_loaded_successfully() = runTest { + val eth = "ethereum" to listOf(ProviderModel.Private(name = "nownodes")) - val remoteEthProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138")) - val remoteProvidersWithEth = remoteProviders + remoteEthProvider + coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse + eth + coEvery { tangemTechApi.getBlockchainProviders() } returns remoteResponse - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEth - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth - everyCrashlyticsRecording() just Runs + // Because configs are merged in BlockchainProvidersResponseMerger + val expected = remoteResponse + eth val actual = loader.load() coVerifyOrder { blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) + tangemTechApi.getBlockchainProviders() } - val expected = remoteProviders + localEthProvider - Truth.assertThat(actual).isEqualTo(expected) } - - @Test - fun test_load_if_configs_contain_public_providers_without_slash_in_the_end() = runTest { - val localPublicProviderUrl = "https://qwe.com" - val localEthProvider = "ethereum" to listOf(ProviderModel.Public(url = localPublicProviderUrl)) - val localProvidersWithEth = localProviders + localEthProvider - - val remotePublicProviderUrl = "https://rty.com" - val remoteDogeProvider = "dogecoin" to listOf(ProviderModel.Public(url = remotePublicProviderUrl)) - val remoteProvidersWithDoge = remoteProviders + remoteDogeProvider - - coEvery { blockchainProvidersStorage.getConfigSync() } returns localProvidersWithEth - coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithDoge - everyCrashlyticsRecording() just Runs - - val actual = loader.load() - - coVerifyOrder { - blockchainProvidersStorage.getConfigSync() - tangemTechServiceApi.getBlockchainProviders() - firebaseCrashlytics.recordException(any()) - } - - val expected = remoteProviders + - localEthProvider.copy(second = listOf(ProviderModel.Public(url = "$localPublicProviderUrl/"))) + - remoteDogeProvider.copy(second = listOf(ProviderModel.Public(url = "$remotePublicProviderUrl/"))) - - Truth.assertThat(actual).isEqualTo(expected) - } - - private fun everyCrashlyticsRecording() = every { firebaseCrashlytics.recordException(any()) } - - private companion object { - val localProviders: BlockchainProvidersResponse = mapOf( - "aptos" to listOf(ProviderModel.Private(name = "nownodes")), - "algorand" to listOf( - ProviderModel.Private(name = "nownodes"), - ProviderModel.Public(url = "https://public_alg.com/"), - ), - ) - - val remoteProviders: BlockchainProvidersResponse = localProviders + mapOf( - "bitcoin" to listOf(ProviderModel.Private(name = "blockchair")), - ) - } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt new file mode 100644 index 0000000000..53004c77b9 --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt @@ -0,0 +1,205 @@ +package com.tangem.blockchainsdk.providers + +import com.google.common.truth.Truth +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.local.config.providers.models.ProviderModel +import io.mockk.* +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class BlockchainProvidersResponseMergerTest { + + @Before + fun setup() { + mockkStatic(FirebaseCrashlytics::class) + val firebaseCrashlytics = mockk() + every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics + every { firebaseCrashlytics.recordException(any()) } just Runs + } + + @Test + fun test_if_both_configs_are_empty() { + val expected = emptyMap>() + + val actual = BlockchainProvidersResponseMerger.merge( + local = emptyMap(), + remote = emptyMap(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_local_config_is_empty() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = emptyMap(), + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_is_empty() { + val expected = localResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = emptyMap(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_both_configs_are_not_empty() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_configs_are_equal() { + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = remoteResponse, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_local_config_has_blockchain_with_empty_providers() { + val eth = "ethereum" to emptyList() + val localResponseWithEth = localResponse + eth + + val expected = localResponseWithEth + remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponseWithEth, + remote = remoteResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_has_blockchain_with_empty_providers() { + val expected = remoteResponse + + val eth = "ethereum" to emptyList() + val remoteWithEth = remoteResponse + eth + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_doesnt_contain_local_providers() { + val remoteWithoutLocal = remoteResponse - localResponse.keys + + /** + * The expected result does not contain local blockchains, since they can be disabled remotely. + * For the opposite case, see [test_if_both_configs_are_not_empty]. + */ + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithoutLocal, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_contains_unsupported_providers() { + val nowNodesProvider = ProviderModel.Private(name = "nownodes") + val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, nowNodesProvider) + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + ("ethereum" to listOf(nowNodesProvider)) + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_remote_config_contains_invalid_public_providers() { + val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138")) + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + + val actual = BlockchainProvidersResponseMerger.merge( + local = localResponse, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_if_configs_contain_public_providers_without_slash_in_the_end() { + val eth = "ethereum" to listOf(ProviderModel.Public("https://qwe.com")) + val kaspa = "kaspa" to listOf(ProviderModel.Public("https://qwe.com")) + + val localWithKaspa = localResponse + kaspa + val remoteWithEth = remoteResponse + eth + + val expected = remoteResponse + eth.addSlash() + kaspa.addSlash() + + val actual = BlockchainProvidersResponseMerger.merge( + local = localWithKaspa, + remote = remoteWithEth, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + companion object { + + val localResponse: BlockchainProvidersResponse = mapOf( + "aptos" to listOf(ProviderModel.Private(name = "nownodes")), + "algorand" to listOf( + ProviderModel.Private(name = "nownodes"), + ProviderModel.Public(url = "https://public_alg.com/"), + ), + ) + + // local + bitcoin + val remoteResponse: BlockchainProvidersResponse = mapOf( + "aptos" to listOf(ProviderModel.Private(name = "nownodes")), + "algorand" to listOf( + ProviderModel.Private(name = "nownodes"), + ProviderModel.Public(url = "https://public_alg.com/"), + ), + "bitcoin" to listOf(ProviderModel.Private(name = "blockchair")), + ) + + private fun Pair>.addSlash(): Pair> { + return first to second.map { it.copy(url = "${it.url}/") } + } + } +} \ No newline at end of file