Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-27 22:06:02 +04:00
parent 5ecc4c6775
commit c38c185a2d
19 changed files with 858 additions and 5 deletions

View file

@ -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<T>(default: T) : DataStore<T> {
private val _data = MutableStateFlow(value = default)
override val data: Flow<T> = _data
override suspend fun updateData(transform: suspend (t: T) -> T): T {
return _data.updateAndGet { transform(it) }
}
}

View file

@ -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))
}
}

View file

@ -10,7 +10,7 @@ import timber.log.Timber
*
[REDACTED_AUTHOR]
*/
internal class NetworkAddressConverter(
class NetworkAddressConverter(
private val selectedAddress: String,
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {

View file

@ -14,7 +14,7 @@ private typealias AmountsDomainModel = Map<CryptoCurrency.ID, CryptoCurrencyAmou
*
[REDACTED_AUTHOR]
*/
internal object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
override fun convert(value: AmountsDataModel): AmountsDomainModel {
return value

View file

@ -10,7 +10,7 @@ import com.tangem.utils.converter.TwoWayConverter
*
[REDACTED_AUTHOR]
*/
internal object NetworkDerivationPathConverter :
object NetworkDerivationPathConverter :
TwoWayConverter<NetworkStatusDM.DerivationPath, Network.DerivationPath> {
override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath {

View file

@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
*
[REDACTED_AUTHOR]
*/
internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
override fun convert(value: NetworkStatus): NetworkStatusDM? {
return when (val status = value.value) {

View file

@ -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

1
data/networks/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -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<NetworkStatusDM, SimpleNetworkStatus> {
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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<String, Set<SimpleNetworkStatus>>
internal typealias WalletIdWithStatusDM = Map<String, Set<NetworkStatusDM>>
/**
* Default implementation of [NetworksStatusesStoreV2]
*
* @property runtimeStore runtime store
* @property persistenceDataStore persistence store
* @param dispatchers dispatchers
*/
internal class DefaultNetworksStatusesStoreV2(
private val runtimeStore: RuntimeSharedStore<WalletIdWithSimpleStatus>,
private val persistenceDataStore: DataStore<WalletIdWithStatusDM>,
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<Set<SimpleNetworkStatus>> {
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
}
}
}
}

View file

@ -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<Set<SimpleNetworkStatus>>
/** 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)
}

View file

@ -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)
}

View file

@ -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<WalletIdWithSimpleStatus>()
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(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<String, Set<NetworkStatusDM>>())
}
@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<String, Set<NetworkStatusDM>>())
}
@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<String, Set<NetworkStatusDM>>())
}
@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<String, Set<NetworkStatusDM>>())
}
private companion object {
val userWalletId = UserWalletId(stringValue = "011")
val network = MockCryptoCurrencyFactory().ethereum.network
}
}

View file

@ -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<WalletIdWithSimpleStatus>()
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(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<Set<SimpleNetworkStatus>>())
}
@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<Set<SimpleNetworkStatus>>())
}
@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<SimpleNetworkStatus>()))
}
@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")
}
}

View file

@ -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<WalletIdWithSimpleStatus>()
val persistenceStore: DataStore<WalletIdWithStatusDM> = 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<WalletIdWithSimpleStatus>()
val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
DefaultNetworksStatusesStoreV2(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap<String, Set<SimpleNetworkStatus>>())
}
@Test
fun `test initialization if cache store is not empty`() = runTest {
val runtimeStore = RuntimeSharedStore<WalletIdWithSimpleStatus>()
val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(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")
}
}

View file

@ -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<WalletIdWithSimpleStatus>()
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(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<String, Set<SimpleNetworkStatus>>,
val persistenceExpected: WalletIdWithStatusDM,
)
private companion object {
val userWalletId = UserWalletId(stringValue = "011")
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Model> {
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(),
),
)
}
}
}

View file

@ -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