Updated on 2026-08-14
This commit is contained in:
parent
820578708c
commit
83d291d7d0
8 changed files with 422 additions and 323 deletions
|
|
@ -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<String, List<ProviderModel>>
|
||||
|
||||
/**
|
||||
* 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<WalletManagerFactory?> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<ProviderModel>.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<ProviderModel>.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<String>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ProviderModel>.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<ProviderModel>.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<String>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.blockchainsdk.providers
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Blockchain providers types manager */
|
||||
interface BlockchainProvidersTypesManager {
|
||||
|
||||
fun get(): StateFlow<BlockchainProviderTypes>
|
||||
|
||||
suspend fun update()
|
||||
}
|
||||
|
|
@ -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<BlockchainProviderTypes> = 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TangemTechApi>()
|
||||
private val tangemTechApi = mockk<TangemTechApi>()
|
||||
private val blockchainProvidersStorage = mockk<BlockchainProvidersStorage>()
|
||||
|
||||
private val loader = BlockchainProvidersResponseLoader(
|
||||
tangemTechServiceApi = tangemTechServiceApi,
|
||||
tangemTechApi = tangemTechApi,
|
||||
blockchainProvidersStorage = blockchainProvidersStorage,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val firebaseCrashlytics = mockk<FirebaseCrashlytics>()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(FirebaseCrashlytics::class)
|
||||
val firebaseCrashlytics = mockk<FirebaseCrashlytics>()
|
||||
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<ProviderModel>()
|
||||
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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FirebaseCrashlytics>()
|
||||
every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics
|
||||
every { firebaseCrashlytics.recordException(any()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_if_both_configs_are_empty() {
|
||||
val expected = emptyMap<String, List<ProviderModel>>()
|
||||
|
||||
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<ProviderModel>()
|
||||
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<ProviderModel>()
|
||||
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<String, List<ProviderModel.Public>>.addSlash(): Pair<String, List<ProviderModel.Public>> {
|
||||
return first to second.map { it.copy(url = "${it.url}/") }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue