Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 22:43:52 +03:00
commit d6f9f59866
1729 changed files with 67614 additions and 9361 deletions

View file

@ -12,11 +12,6 @@ plugins {
android {
namespace = "com.tangem.data.staking"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Core modules */
implementation(projects.core.datasource)
@ -69,7 +64,6 @@ dependencies {
// endregion
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(tangemDeps.card.core)
testImplementation(projects.common.test)
testImplementation(projects.test.core)

View file

@ -12,18 +12,26 @@ import com.tangem.domain.models.staking.*
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
import app.cash.turbine.test
import com.tangem.test.core.TestFlowProducerTools
import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultMultiStakingBalanceProducerTest {
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
@ -41,6 +49,25 @@ internal class DefaultMultiStakingBalanceProducerTest {
dispatchers = dispatchers,
)
// Producer wired with a real test FlowProducerTools (shareIn + retry + distinctUntilChanged)
// for produceWithFallback() cases.
private fun TestScope.createProducer(): DefaultMultiStakingBalanceProducer {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
return DefaultMultiStakingBalanceProducer(
params = params,
stakeKitBalancesStore = stakeKitBalancesStore,
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
flowProducerTools = TestFlowProducerTools(scope = backgroundScope, dispatcher = testDispatcher),
dispatchers = TestingCoroutineDispatcherProvider(
main = testDispatcher,
mainImmediate = testDispatcher,
io = testDispatcher,
default = testDispatcher,
single = testDispatcher,
),
)
}
@Test
fun `test that flow is mapped for user wallet id from params`() = runTest {
val balances = setOf(
@ -113,32 +140,26 @@ internal class DefaultMultiStakingBalanceProducerTest {
every { stakeKitBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
val actual = createProducer().produceWithFallback()
// check after producer.produce()
verify { stakeKitBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit
val wrappers = setOf(
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
)
actual.test {
val wrappers = setOf(
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(),
MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(),
)
networksStatusesFlow.emit(wrappers)
networksStatusesFlow.emit(wrappers)
Truth.assertThat(awaitItem()).isEqualTo(wrappers)
val values1 = getEmittedValues(flow = actual)
// same balances again -> filtered out by distinctUntilChanged
networksStatusesFlow.emit(wrappers)
expectNoEvents()
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1.first()).isEqualTo(wrappers)
// second emit
networksStatusesFlow.emit(wrappers)
val values2 = getEmittedValues(flow = actual)
Truth.assertThat(values2.size).isEqualTo(1)
Truth.assertThat(values2.first()).isEqualTo(wrappers)
cancelAndIgnoreRemainingEvents()
}
}
@Test
@ -162,23 +183,24 @@ internal class DefaultMultiStakingBalanceProducerTest {
every { stakeKitBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produceWithFallback()
val actual = createProducer().produceWithFallback()
// check after producer.produce()
verify { stakeKitBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values1 = getEmittedValues(flow = actual)
actual.test {
// first collection throws -> retryWhen emits the empty fallback, then waits 2s
Truth.assertThat(awaitItem()).isEqualTo(emptySet<StakingBalance>())
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1).isEqualTo(listOf(emptySet<StakingBalance>()))
// recover the upstream and let the retry fire
innerFlow.value = true
advanceTimeBy(delayTimeMillis = 2001)
runCurrent()
innerFlow.emit(value = true)
Truth.assertThat(awaitItem()).isEqualTo(balances)
val values2 = getEmittedValues(flow = actual)
Truth.assertThat(values2.size).isEqualTo(1)
Truth.assertThat(values2).isEqualTo(listOf(balances))
cancelAndIgnoreRemainingEvents()
}
}
@Test

View file

@ -11,22 +11,29 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
import app.cash.turbine.test
import com.tangem.test.core.TestFlowProducerTools
import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultSingleStakingBalanceProducerTest {
@ -48,6 +55,23 @@ internal class DefaultSingleStakingBalanceProducerTest {
flowProducerTools = flowProducerTools,
)
private fun TestScope.createProducer(): DefaultSingleStakingBalanceProducer {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
return DefaultSingleStakingBalanceProducer(
params = params,
multiStakingBalanceSupplier = multiNetworkStatusSupplier,
analyticsExceptionHandler = analyticsExceptionHandler,
dispatchers = TestingCoroutineDispatcherProvider(
main = testDispatcher,
mainImmediate = testDispatcher,
io = testDispatcher,
default = testDispatcher,
single = testDispatcher,
),
flowProducerTools = TestFlowProducerTools(scope = backgroundScope, dispatcher = testDispatcher),
)
}
@BeforeEach
fun resetMocks() {
clearMocks(multiNetworkStatusSupplier, analyticsExceptionHandler)
@ -77,7 +101,6 @@ internal class DefaultSingleStakingBalanceProducerTest {
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
}
@Disabled
@Test
fun `flow is updated if staking balance is updated`() = runTest {
// Arrange
@ -86,31 +109,23 @@ internal class DefaultSingleStakingBalanceProducerTest {
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
val producerFlow = producer.produceWithFallback()
val producerFlow = createProducer().produceWithFallback()
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
val updatedBalance = StakingBalance.Error(stakingId = tonId)
producerFlow.test {
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
multiFlow.emit(value = setOf(balance))
Truth.assertThat(awaitItem()).isEqualTo(balance)
// Act (first emit)
multiFlow.emit(value = setOf(balance))
val actual1 = getEmittedValues(flow = producerFlow)
val updatedBalance = StakingBalance.Error(stakingId = tonId)
multiFlow.emit(value = setOf(updatedBalance))
Truth.assertThat(awaitItem()).isEqualTo(updatedBalance)
// Assert (first emit)
Truth.assertThat(actual1).hasSize(1)
Truth.assertThat(actual1).containsExactly(balance)
// Act (second emit)
multiFlow.emit(value = setOf(updatedBalance))
val actual2 = getEmittedValues(flow = producerFlow)
// Assert (second emit)
Truth.assertThat(actual2).hasSize(2)
Truth.assertThat(actual2).containsExactly(balance, updatedBalance)
cancelAndIgnoreRemainingEvents()
}
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
}
@Disabled
@Test
fun `flow is filtered the same status`() = runTest {
// Arrange
@ -119,30 +134,23 @@ internal class DefaultSingleStakingBalanceProducerTest {
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
val producerFlow = producer.produceWithFallback()
val producerFlow = createProducer().produceWithFallback()
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
producerFlow.test {
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
multiFlow.emit(value = setOf(balance))
Truth.assertThat(awaitItem()).isEqualTo(balance)
// Act (first emit)
multiFlow.emit(value = setOf(balance))
val actual1 = getEmittedValues(flow = producerFlow)
// same balance again -> filtered out by distinctUntilChanged
multiFlow.emit(value = setOf(balance))
expectNoEvents()
// Assert (first emit)
Truth.assertThat(actual1).hasSize(1)
Truth.assertThat(actual1).containsExactly(balance)
// Act (second emit)
multiFlow.emit(value = setOf(balance))
val actual2 = getEmittedValues(flow = producerFlow)
// Assert (second emit)
Truth.assertThat(actual2).hasSize(1)
Truth.assertThat(actual2).containsExactly(balance)
cancelAndIgnoreRemainingEvents()
}
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
}
@Disabled
@Test
fun `flow throws exception`() = runTest {
// Arrange
@ -163,23 +171,22 @@ internal class DefaultSingleStakingBalanceProducerTest {
val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns multiFlow
val producerFlow = producer.produceWithFallback()
val producerFlow = createProducer().produceWithFallback()
// Act (first emit)
val actual1 = getEmittedValues(flow = producerFlow)
producerFlow.test {
// first collection throws -> retryWhen emits the fallback, then waits 2s
val fallbackStatus = StakingBalance.Error(stakingId = tonId.copy(address = "0x1"))
Truth.assertThat(awaitItem()).isEqualTo(fallbackStatus)
// Assert (first emit)
val fallbackStatus = StakingBalance.Error(stakingId = tonId.copy(address = "0x1"))
// recover the upstream and let the retry fire
innerFlow.value = true
advanceTimeBy(delayTimeMillis = 2001)
runCurrent()
Truth.assertThat(actual1).hasSize(1)
Truth.assertThat(actual1).containsExactly(fallbackStatus)
Truth.assertThat(awaitItem()).isEqualTo(balance)
// Act (second emit)
innerFlow.emit(value = true)
val actual2 = getEmittedValues(flow = producerFlow)
Truth.assertThat(actual2).hasSize(1)
Truth.assertThat(actual2).containsExactly(balance)
cancelAndIgnoreRemainingEvents()
}
verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) }
}

View file

@ -1,16 +1,16 @@
package com.tangem.data.staking.store
import com.google.common.truth.Truth
import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.test.core.TestAppCoroutineScope
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.test.core.datastore.MockStateDataStore
import com.tangem.data.staking.toDomain
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.getEmittedValues
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]

View file

@ -2,9 +2,9 @@ package com.tangem.data.staking.store
import androidx.datastore.core.DataStore
import com.google.common.truth.Truth
import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.test.core.TestAppCoroutineScope
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.test.core.datastore.MockStateDataStore
import com.tangem.data.staking.toDomain
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.staking.StakingBalance
@ -13,7 +13,7 @@ import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]

View file

@ -1,9 +1,9 @@
package com.tangem.data.staking.store
import com.google.common.truth.Truth
import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.test.core.TestAppCoroutineScope
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.test.core.datastore.MockStateDataStore
import com.tangem.data.staking.toDomain
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.RuntimeSharedStore
@ -13,7 +13,7 @@ import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.Test
/**
[REDACTED_AUTHOR]
@ -148,8 +148,10 @@ internal class StakingBalancesStoreUpdateMethodsTest {
store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId))
// storeError keeps the existing (CACHE) balance and downgrades its source to ONLY_CACHE.
// toDomain(ONLY_CACHE) can't express this: the converter maps any non-CACHE source to ACTUAL.
val runtimeExpected = mapOf(
userWalletId to setOf(wrapper.toDomain(source = StatusSource.ONLY_CACHE)),
userWalletId to setOf(wrapper.toDomain(source = StatusSource.CACHE).copySealed(source = StatusSource.ONLY_CACHE)),
)
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)