Updated on 2026-08-14
This commit is contained in:
commit
d6f9f59866
1729 changed files with 67614 additions and 9361 deletions
|
|
@ -8,13 +8,10 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.account"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(projects.features.virtualAccounts.details.api) // VIRTUAL_ACCOUNTS_ENABLED
|
||||
|
||||
// region Project - Common
|
||||
implementation(projects.common.ui) // It's needed for getting AccountName.DefaultMain value
|
||||
// endregion
|
||||
|
|
@ -70,7 +67,6 @@ dependencies {
|
|||
// region Test
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.turbine)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
internal fun String.toAccountId(userWalletId: UserWalletId): AccountId {
|
||||
return when {
|
||||
startsWith(AccountId.PaymentAccountIdPrefix) -> AccountId.forPaymentAccount(userWalletId).right()
|
||||
startsWith(AccountId.VirtualAccountIdPrefix) -> AccountId.forVirtualAccount(userWalletId).right()
|
||||
else -> AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId)
|
||||
}.getOrElse {
|
||||
error("Unable to create AccountId from value: $this. Cause: $it")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.common.wallets.getSyncStrict
|
|||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -37,6 +38,7 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
override val flowProducerTools: FlowProducerTools,
|
||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleAccountListProducer {
|
||||
|
||||
|
|
@ -51,18 +53,9 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
return walletAccountListFlowFactory.create(walletId)
|
||||
.map { accountList ->
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(id = walletId)
|
||||
val isPaymentSupported = userWallet.isPaymentAccountSupported()
|
||||
logger.i(
|
||||
"produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " +
|
||||
"isPaymentAccountSupported=$isPaymentSupported",
|
||||
)
|
||||
if (isPaymentSupported) {
|
||||
accountList.plus(Account.Payment(walletId)).getOrElse { throwable ->
|
||||
error("Can not combine account list and payment account status: $throwable")
|
||||
}
|
||||
} else {
|
||||
accountList
|
||||
}
|
||||
accountList
|
||||
.addAccountIf(userWallet.isPaymentAccountSupported()) { Account.Payment(walletId) }
|
||||
.addAccountIf(userWallet.isVirtualAccountSupported()) { Account.Virtual(walletId) }
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
|
@ -72,6 +65,25 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
|||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||
}
|
||||
|
||||
private fun UserWallet.isVirtualAccountSupported(): Boolean {
|
||||
if (!virtualAccountsFeatureToggles.isVirtualAccountsEnabled) return false
|
||||
|
||||
return when (this) {
|
||||
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun AccountList.addAccountIf(condition: Boolean, account: () -> Account): AccountList {
|
||||
return if (condition) {
|
||||
plus(account()).getOrElse { throwable ->
|
||||
error("Can not combine account list and special account: $throwable")
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SingleAccountListProducer.Factory {
|
||||
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
|
||||
|
|
|
|||
|
|
@ -335,7 +335,10 @@ class DefaultWalletAccountsFetcherTest {
|
|||
body = SaveWalletAccountsResponse(savedAccountsResponse.accounts),
|
||||
)
|
||||
eTagsStore.clear(userWalletId, ETagsStore.Key.WalletAccounts)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = savedAccountsResponse.toUserTokensResponse(),
|
||||
)
|
||||
tokensMigration.migrate(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,27 @@ import com.tangem.domain.core.flow.FlowProducerTools
|
|||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
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.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultMultiAccountListProducerTest {
|
||||
|
|
@ -40,6 +45,23 @@ class DefaultMultiAccountListProducerTest {
|
|||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private fun TestScope.createProducer(): DefaultMultiAccountListProducer {
|
||||
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
return DefaultMultiAccountListProducer(
|
||||
params = Unit,
|
||||
flowProducerTools = TestFlowProducerTools(scope = backgroundScope, dispatcher = testDispatcher),
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
|
|
@ -144,7 +166,6 @@ class DefaultMultiAccountListProducerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
fun `flow returns empty list if factory throws exception`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -154,12 +175,12 @@ class DefaultMultiAccountListProducerTest {
|
|||
val exception = RuntimeException("Converter error")
|
||||
every { singleAccountListSupplier.invoke(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = producer.produceWithFallback().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = emptyList<AccountList>()
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
// Act / Assert: the factory throws -> retryWhen emits the empty fallback.
|
||||
// Stop before the 2s retry fires so the upstream is collected exactly once.
|
||||
createProducer().produceWithFallback().test {
|
||||
Truth.assertThat(awaitItem()).isEqualTo(emptyList<AccountList>())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
userWalletsListRepository.load()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -40,12 +43,16 @@ class DefaultSingleAccountListProducerTest {
|
|||
private val userWalletsListRepository = mockk<UserWalletsListRepository> {
|
||||
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(value = listOf(userWallet))
|
||||
}
|
||||
private val virtualAccountsFeatureToggles = mockk<VirtualAccountFeatureToggles> {
|
||||
every { isVirtualAccountsEnabled } returns false
|
||||
}
|
||||
|
||||
private val producer = DefaultSingleAccountListProducer(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||
flowProducerTools = flowProducerTools,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
@ -72,6 +79,45 @@ class DefaultSingleAccountListProducerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN virtual accounts enabled WHEN produce THEN account list contains virtual account`() = runTest {
|
||||
// Arrange
|
||||
val supportedWallet = mockk<UserWallet.Hot> {
|
||||
every { walletId } returns userWalletId
|
||||
every { hotWalletId } returns mockk {
|
||||
every { authType } returns HotWalletId.AuthType.Password
|
||||
}
|
||||
}
|
||||
val userWalletsListRepository = mockk<UserWalletsListRepository> {
|
||||
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(value = listOf(supportedWallet))
|
||||
}
|
||||
val virtualAccountsFeatureToggles = mockk<VirtualAccountFeatureToggles> {
|
||||
every { isVirtualAccountsEnabled } returns true
|
||||
}
|
||||
val producer = DefaultSingleAccountListProducer(
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||
flowProducerTools = flowProducerTools,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = accountList
|
||||
.plus(Account.Payment(userWalletId))
|
||||
.getOrElse { error("Unable to add payment account: $it") }
|
||||
.plus(Account.Virtual(userWalletId))
|
||||
.getOrElse { error("Unable to add virtual account: $it") }
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if factoryFlow is updated`() = runTest {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
|||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.Account.CryptoPortfolio
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.account.repository
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
|
|
@ -21,7 +21,7 @@ class DefaultAccountsExpandedRepositoryTest {
|
|||
@Test
|
||||
fun `expandedAccounts emits updated state when store changes`() = runTest {
|
||||
val dataStore = MockStateDataStore<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
default = emptyMap()
|
||||
default = emptyMap(),
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
|
@ -37,9 +37,9 @@ class DefaultAccountsExpandedRepositoryTest {
|
|||
walletId.stringValue to setOf(
|
||||
AccountsExpandedDTO(
|
||||
accountId = mainAccountId.value,
|
||||
isExpanded = true
|
||||
)
|
||||
)
|
||||
isExpanded = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -60,14 +60,14 @@ class DefaultAccountsExpandedRepositoryTest {
|
|||
@Test
|
||||
fun `expandedAccounts emits when update is called`() = runTest {
|
||||
val dataStore = MockStateDataStore<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
default = emptyMap()
|
||||
default = emptyMap(),
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
||||
val state = AccountExpandedState(
|
||||
accountId = mainAccountId,
|
||||
isExpanded = true
|
||||
isExpanded = true,
|
||||
)
|
||||
|
||||
repository.expandedAccounts.test {
|
||||
|
|
@ -94,9 +94,9 @@ class DefaultAccountsExpandedRepositoryTest {
|
|||
mapOf(
|
||||
walletId.stringValue to setOf(
|
||||
AccountsExpandedDTO(mainAccountId.value, true),
|
||||
AccountsExpandedDTO(secondAccountId.value, false)
|
||||
)
|
||||
)
|
||||
AccountsExpandedDTO(secondAccountId.value, false),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
|
@ -109,7 +109,7 @@ class DefaultAccountsExpandedRepositoryTest {
|
|||
// when
|
||||
repository.syncStore(
|
||||
walletId = walletId,
|
||||
existAccounts = setOf(mainAccountId) // without secondAccountId
|
||||
existAccounts = setOf(mainAccountId), // without secondAccountId
|
||||
)
|
||||
|
||||
// then
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.account.store
|
|||
import android.content.Context
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.mockk
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.account.token
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.card.common
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
class TwinsHelperTest {
|
||||
|
|
@ -18,7 +17,7 @@ class TwinsHelperTest {
|
|||
|
||||
@Test
|
||||
fun `twins compatibility pack 1 success`() {
|
||||
Assert.assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1]))
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1]))
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[0]))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ dependencies {
|
|||
|
||||
/* Tests */
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.turbine)
|
||||
testImplementation(deps.test.truth)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.domain.blockaid.models.transaction.simultation.ApproveInfo
|
|||
import com.domain.blockaid.models.transaction.simultation.SimulationData
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.*
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BlockAidMapperTest {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
|||
import io.mockk.*
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultBlockAidRepositoryTest {
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ class DefaultBlockAidRepositoryTest {
|
|||
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
repository = DefaultBlockAidRepository(api, dispatchers, mapper)
|
||||
|
|
|
|||
|
|
@ -30,4 +30,8 @@ dependencies {
|
|||
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// end
|
||||
}
|
||||
|
|
@ -32,33 +32,9 @@ internal class DefaultCardRepository(
|
|||
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
|
||||
}
|
||||
|
||||
override suspend fun finishCardActivation(cardId: String) {
|
||||
override suspend fun finishCardActivation(cardId: String, hasBackupError: Boolean) {
|
||||
appPreferencesStore.editUsedCards(cardId) {
|
||||
it.copy(isActivationStarted = true, isActivationFinished = true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun finishCardsActivation(cardIds: List<String>) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val usedCards = mutablePreferences.getUsedCards()
|
||||
|
||||
val newCards = cardIds.mapNotNull { newCardId ->
|
||||
if (usedCards.none { it.cardId == newCardId }) {
|
||||
createDefaultUsedCardInfo(cardId = newCardId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val updatedUsedCards = (usedCards + newCards).map { card ->
|
||||
if (cardIds.contains(card.cardId)) {
|
||||
card.copy(isActivationStarted = true, isActivationFinished = true)
|
||||
} else {
|
||||
card
|
||||
}
|
||||
}
|
||||
|
||||
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
|
||||
it.copy(isActivationStarted = true, isActivationFinished = true, hasBackupError = hasBackupError)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +52,10 @@ internal class DefaultCardRepository(
|
|||
return card.isActivationStarted && !card.isActivationFinished
|
||||
}
|
||||
|
||||
override suspend fun hasBackupError(cardId: String): Boolean {
|
||||
return getUsedCardSync(cardId)?.hasBackupError == true
|
||||
}
|
||||
|
||||
override suspend fun isTangemTOSAccepted(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
package com.tangem.data.card
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.card.UsedCardInfo
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectList
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
* Tests for [DefaultCardRepository].
|
||||
*
|
||||
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] and a real [Moshi]
|
||||
* instance, so the JSON round-trip through preferences is exercised end-to-end.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultCardRepositoryTest {
|
||||
|
||||
// Only the in-memory store's content is mutable, so it is the single thing reset per test.
|
||||
private val dataStore = MockStateDataStore(default = emptyPreferences())
|
||||
private val appPreferencesStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = dataStore,
|
||||
)
|
||||
private val repository = DefaultCardRepository(appPreferencesStore)
|
||||
|
||||
@BeforeEach
|
||||
fun resetStore() {
|
||||
runBlocking { dataStore.updateData { emptyPreferences() } }
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class WasCardScanned {
|
||||
|
||||
@Test
|
||||
fun `GIVEN card present WHEN wasCardScanned THEN emits true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID))
|
||||
|
||||
// Act
|
||||
val result = repository.wasCardScanned(CARD_ID).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN wasCardScanned THEN emits false`() = runTest {
|
||||
// Act
|
||||
val result = repository.wasCardScanned(CARD_ID).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class SetCardWasScanned {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN setCardWasScanned THEN creates entry with isScanned true`() = runTest {
|
||||
// Act
|
||||
repository.setCardWasScanned(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isScanned = true))
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class StartCardActivation {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN startCardActivation THEN creates entry with isActivationStarted true`() = runTest {
|
||||
// Act
|
||||
repository.startCardActivation(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN other cards present WHEN editing one card THEN others are preserved`() = runTest {
|
||||
// Arrange
|
||||
val other = UsedCardInfo(cardId = OTHER_CARD_ID, isScanned = true)
|
||||
seedCards(other)
|
||||
|
||||
// Act
|
||||
repository.startCardActivation(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
other,
|
||||
UsedCardInfo(cardId = CARD_ID, isActivationStarted = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FinishCardActivation {
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN finishCardActivation with backup error THEN entry marked finished with error`() =
|
||||
runTest {
|
||||
// Act
|
||||
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = true)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
UsedCardInfo(
|
||||
cardId = CARD_ID,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN finishCardActivation without backup error THEN entry marked finished without error`() =
|
||||
runTest {
|
||||
// Act
|
||||
repository.finishCardActivation(cardId = CARD_ID, hasBackupError = false)
|
||||
|
||||
// Assert
|
||||
assertThat(storedCards()).containsExactly(
|
||||
UsedCardInfo(
|
||||
cardId = CARD_ID,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
hasBackupError = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationStarted {
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation started WHEN isActivationStarted THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationStarted = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isActivationStarted(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN isActivationStarted THEN false`() = runTest {
|
||||
assertThat(repository.isActivationStarted(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationFinished {
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation finished WHEN isActivationFinished THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, isActivationFinished = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isActivationFinished(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card absent WHEN isActivationFinished THEN false`() = runTest {
|
||||
assertThat(repository.isActivationFinished(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsActivationInProgress {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isActivationInProgress(model: ActivationInProgressModel) = runTest {
|
||||
// Arrange
|
||||
model.stored?.let { seedCards(it) }
|
||||
|
||||
// Act
|
||||
val result = repository.isActivationInProgress(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ActivationInProgressModel(stored = null, expected = false),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = false),
|
||||
expected = false,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = false),
|
||||
expected = true,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = true, isActivationFinished = true),
|
||||
expected = false,
|
||||
),
|
||||
ActivationInProgressModel(
|
||||
stored = UsedCardInfo(cardId = CARD_ID, isActivationStarted = false, isActivationFinished = true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class HasBackupError {
|
||||
|
||||
@Test
|
||||
fun `GIVEN backup error WHEN hasBackupError THEN true`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = true))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.hasBackupError(CARD_ID)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no backup error WHEN hasBackupError THEN false`() = runTest {
|
||||
// Arrange
|
||||
seedCards(UsedCardInfo(cardId = CARD_ID, hasBackupError = false))
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.hasBackupError(CARD_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class TangemTos {
|
||||
|
||||
@Test
|
||||
fun `GIVEN nothing stored WHEN isTangemTOSAccepted THEN false by default`() = runTest {
|
||||
assertThat(repository.isTangemTOSAccepted()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN TOS accepted WHEN isTangemTOSAccepted THEN true`() = runTest {
|
||||
// Arrange
|
||||
repository.acceptTangemTOS()
|
||||
|
||||
// Act & Assert
|
||||
assertThat(repository.isTangemTOSAccepted()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedCards(vararg cards: UsedCardInfo) {
|
||||
appPreferencesStore.storeObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = cards.toList())
|
||||
}
|
||||
|
||||
private suspend fun storedCards(): List<UsedCardInfo> {
|
||||
return appPreferencesStore.getObjectListSync(key = PreferencesKeys.USED_CARDS_INFO_KEY)
|
||||
}
|
||||
|
||||
internal data class ActivationInProgressModel(val stored: UsedCardInfo?, val expected: Boolean)
|
||||
|
||||
private companion object {
|
||||
const val CARD_ID = "card-1"
|
||||
const val OTHER_CARD_ID = "card-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.common"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -49,6 +44,5 @@ dependencies {
|
|||
/* Test */
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.moshi)
|
||||
}
|
||||
|
|
@ -8,11 +8,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.dynamicaddresses"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -44,7 +39,6 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Testing
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ import com.tangem.data.express.converter.ExpressProviderConverter
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
|
||||
import com.tangem.datasource.local.converter.toEntity
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
|
|
@ -16,6 +18,7 @@ import com.tangem.utils.logging.TangemLogger
|
|||
|
||||
internal class DefaultExpressRepository(
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ExpressRepository {
|
||||
|
|
@ -26,13 +29,17 @@ internal class DefaultExpressRepository(
|
|||
): List<ExpressProvider> = with(dispatchers.io) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
tangemExpressApi.getProviders(
|
||||
val providers = tangemExpressApi.getProviders(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
refCode = ExpressUtils.getRefCode(
|
||||
userWallet = userWallet,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
),
|
||||
).getOrThrow().map(ExpressProviderConverter()::convert)
|
||||
).getOrThrow()
|
||||
|
||||
expressHistoryDao.upsertProviders(providers.map { it.toEntity() })
|
||||
|
||||
providers.map(ExpressProviderConverter()::convert)
|
||||
.filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes }
|
||||
},
|
||||
onError = { error ->
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.datasource.api.express.TangemExpressApi
|
|||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.domain.express.ExpressErrorResolver
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
|
|
@ -36,11 +37,13 @@ internal object ExpressDataModule {
|
|||
@Singleton
|
||||
fun provideExpressRepository(
|
||||
tangemExpressApi: TangemExpressApi,
|
||||
expressHistoryDao: ExpressHistoryDao,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ExpressRepository {
|
||||
return DefaultExpressRepository(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
expressHistoryDao = expressHistoryDao,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.networks"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -49,7 +44,6 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Tests
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(tangemDeps.blockchain)
|
||||
testImplementation(tangemDeps.card.core)
|
||||
testImplementation(projects.common.test)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer)
|
||||
clearMocks(
|
||||
networksStatusesStore,
|
||||
cardCryptoCurrencyFactory,
|
||||
commonNetworkStatusFetcher,
|
||||
dynamicAddressesInitializer,
|
||||
)
|
||||
// No dynamic addresses restore by default
|
||||
coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.networks.multi
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
|
|
@ -11,23 +12,28 @@ import com.tangem.data.networks.store.NetworksStatusesStore
|
|||
import com.tangem.data.networks.toSimple
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncOrNull
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.test.core.TestFlowProducerTools
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
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 DefaultMultiNetworkStatusProducerTest {
|
||||
|
||||
|
|
@ -48,6 +54,24 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
flowProducerTools = flowProducerTools,
|
||||
)
|
||||
|
||||
private fun TestScope.createProducer(): DefaultMultiNetworkStatusProducer {
|
||||
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
return DefaultMultiNetworkStatusProducer(
|
||||
params = params,
|
||||
networksStatusesStore = networksStatusesStore,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
networkFactory = networkFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
),
|
||||
flowProducerTools = TestFlowProducerTools(scope = backgroundScope, dispatcher = testDispatcher),
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(networksStatusesStore, userWalletsListRepository, networkFactory)
|
||||
|
|
@ -66,7 +90,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
val networksStatusesFlow = flowOf(simpleStatuses)
|
||||
|
||||
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns userWalletsFlow
|
||||
|
||||
|
|
@ -132,7 +156,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
|
||||
// region every
|
||||
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns userWalletsFlow
|
||||
every {
|
||||
|
|
@ -235,7 +259,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
|
||||
// region every
|
||||
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns userWalletsFlow
|
||||
|
||||
|
|
@ -294,7 +318,6 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
Truth.assertThat(actual2.first()).isEqualTo(expected2)
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
fun `flow throws exception`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -319,7 +342,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
|
||||
// region every
|
||||
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns userWalletsFlow
|
||||
every {
|
||||
|
|
@ -338,30 +361,26 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
} returns statuses.last().network
|
||||
// endregion
|
||||
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
val producerFlow = createProducer().produceWithFallback()
|
||||
|
||||
// Act 1 (fallback)
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
producerFlow.test {
|
||||
// first collection throws -> retryWhen emits the empty fallback, then waits 2s
|
||||
Truth.assertThat(awaitItem()).isEqualTo(emptySet<NetworkStatus>())
|
||||
|
||||
// Assert
|
||||
val expected1 = emptySet<NetworkStatus>()
|
||||
Truth.assertThat(actual1.size).isEqualTo(1)
|
||||
Truth.assertThat(actual1.first()).isEqualTo(expected1)
|
||||
verify(inverse = true) {
|
||||
networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any())
|
||||
}
|
||||
|
||||
verifyOrder(inverse = true) {
|
||||
userWalletsListRepository.getSyncOrNull(any())
|
||||
networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any())
|
||||
// recover the upstream and let the retry fire
|
||||
innerFlow.value = true
|
||||
advanceTimeBy(delayTimeMillis = 2001)
|
||||
runCurrent()
|
||||
|
||||
Truth.assertThat(awaitItem()).isEqualTo(statuses)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
// Act 2 (emit)
|
||||
innerFlow.emit(value = true)
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected2 = statuses
|
||||
Truth.assertThat(actual2.size).isEqualTo(1)
|
||||
Truth.assertThat(actual2.first()).isEqualTo(expected2)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsListRepository.userWallets
|
||||
networkFactory.create(
|
||||
|
|
@ -407,7 +426,7 @@ internal class DefaultMultiNetworkStatusProducerTest {
|
|||
val networksStatusesFlow = flowOf(simpleStatuses)
|
||||
|
||||
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns userWalletsFlow
|
||||
coEvery { networkFactory.create(networkId = any(), any(), any()) } returns null
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
package com.tangem.data.networks.single
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
|
||||
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 DefaultSingleNetworkStatusProducerTest {
|
||||
|
||||
private val params = SingleNetworkStatusProducer.Params(
|
||||
|
|
@ -29,15 +36,22 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
)
|
||||
|
||||
private val multiNetworkStatusSupplier = mockk<MultiNetworkStatusSupplier>()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
private val flowProducerTools: FlowProducerTools = mockk()
|
||||
|
||||
private val producer = DefaultSingleNetworkStatusProducer(
|
||||
params = params,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
dispatchers = dispatchers,
|
||||
flowProducerTools = flowProducerTools,
|
||||
)
|
||||
private fun TestScope.createProducer(): DefaultSingleNetworkStatusProducer {
|
||||
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
return DefaultSingleNetworkStatusProducer(
|
||||
params = params,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
),
|
||||
flowProducerTools = TestFlowProducerTools(scope = backgroundScope, dispatcher = testDispatcher),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that flow is mapped for network from params`() = runTest {
|
||||
|
|
@ -52,7 +66,7 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
|
||||
val actual = producer.produce()
|
||||
val actual = createProducer().produce()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
|
|
@ -69,27 +83,21 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
// first emit
|
||||
val status = MockNetworkStatusFactory.createMissedDerivation(params.network)
|
||||
expected.emit(value = setOf(status))
|
||||
actual.test {
|
||||
val status = MockNetworkStatusFactory.createMissedDerivation(params.network)
|
||||
expected.emit(value = setOf(status))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
val updatedStatus = status.copy(value = NetworkStatus.Unreachable(null))
|
||||
expected.emit(value = setOf(updatedStatus))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(updatedStatus)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(status))
|
||||
|
||||
// second emit
|
||||
val updatedStatus = status.copy(value = NetworkStatus.Unreachable(null))
|
||||
expected.emit(value = setOf(updatedStatus))
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -99,26 +107,21 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
// first emit
|
||||
val status = MockNetworkStatusFactory.createMissedDerivation(params.network)
|
||||
expected.emit(value = setOf(status))
|
||||
actual.test {
|
||||
val status = MockNetworkStatusFactory.createMissedDerivation(params.network)
|
||||
expected.emit(value = setOf(status))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
// same status again -> filtered out by distinctUntilChanged
|
||||
expected.emit(value = setOf(status))
|
||||
expectNoEvents()
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(status))
|
||||
|
||||
// second emit
|
||||
expected.emit(value = setOf(status))
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -139,21 +142,24 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
actual.test {
|
||||
// first collection throws -> retryWhen emits the fallback, then waits 2s before retrying
|
||||
val fallbackStatus = MockNetworkStatusFactory.createUnreachable(params.network)
|
||||
Truth.assertThat(awaitItem()).isEqualTo(fallbackStatus)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
val fallbackStatus = MockNetworkStatusFactory.createUnreachable(params.network)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus))
|
||||
// recover the upstream and let the retry fire
|
||||
innerFlow.value = true
|
||||
advanceTimeBy(delayTimeMillis = 2001)
|
||||
runCurrent()
|
||||
|
||||
innerFlow.emit(value = true)
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -165,7 +171,7 @@ internal class DefaultSingleNetworkStatusProducerTest {
|
|||
val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId)
|
||||
every { multiNetworkStatusSupplier(multiParams) } returns expected
|
||||
|
||||
val actual = producer.produce()
|
||||
val actual = createProducer().produce()
|
||||
|
||||
verify { multiNetworkStatusSupplier(multiParams) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.data.networks.store
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.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
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.test.core.getEmittedValues
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.data.networks.store
|
|||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -15,7 +15,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]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -11,18 +11,16 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
internal class ParameterizedStoreStatusTest(private val model: Model) {
|
||||
internal class ParameterizedStoreStatusTest {
|
||||
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithSimpleStatus>()
|
||||
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
|
||||
|
|
@ -34,8 +32,9 @@ internal class ParameterizedStoreStatusTest(private val model: Model) {
|
|||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test store success`() = runTest {
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `test store success`(model: Model) = runTest {
|
||||
val actual = runCatching { store.storeStatus(userWalletId = userWalletId, status = model.status) }
|
||||
|
||||
Truth.assertThat(actual.isSuccess).isEqualTo(model.isSuccess)
|
||||
|
|
@ -55,8 +54,7 @@ internal class ParameterizedStoreStatusTest(private val model: Model) {
|
|||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Model> {
|
||||
fun provideTestModels(): Collection<Model> {
|
||||
return listOf(
|
||||
// region any network statuses with StatusSource.ACTUAL
|
||||
MockNetworkStatusFactory.createVerified(source = StatusSource.ACTUAL).let { status ->
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -11,18 +11,16 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
internal class ParameterizedStoreSuccessTest(private val model: Model) {
|
||||
internal class ParameterizedStoreSuccessTest {
|
||||
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithSimpleStatus>()
|
||||
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
|
||||
|
|
@ -34,8 +32,9 @@ internal class ParameterizedStoreSuccessTest(private val model: Model) {
|
|||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test store success`() = runTest {
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `test store success`(model: Model) = runTest {
|
||||
val actual = runCatching { store.storeSuccess(userWalletId = userWalletId, status = model.status) }
|
||||
|
||||
Truth.assertThat(actual.isSuccess).isEqualTo(model.isSuccess)
|
||||
|
|
@ -55,8 +54,7 @@ internal class ParameterizedStoreSuccessTest(private val model: Model) {
|
|||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Model> {
|
||||
fun provideTestModels(): Collection<Model> {
|
||||
return listOf(
|
||||
// region any network statuses with StatusSource.ACTUAL
|
||||
MockNetworkStatusFactory.createVerified(source = StatusSource.ACTUAL).let { status ->
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -10,18 +10,16 @@ import com.tangem.data.networks.toSimple
|
|||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
internal class ParameterizedStoreTest(private val model: Model) {
|
||||
internal class ParameterizedStoreTest {
|
||||
|
||||
private val runtimeStore = RuntimeSharedStore<WalletIdWithSimpleStatus>()
|
||||
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
|
||||
|
|
@ -33,8 +31,9 @@ internal class ParameterizedStoreTest(private val model: Model) {
|
|||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test store method`() = runTest {
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `test store method`(model: Model) = runTest {
|
||||
store.store(userWalletId = userWalletId, status = model.status)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected)
|
||||
|
|
@ -52,8 +51,7 @@ internal class ParameterizedStoreTest(private val model: Model) {
|
|||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Model> {
|
||||
fun provideTestModels(): Collection<Model> {
|
||||
return listOf(
|
||||
MockNetworkStatusFactory.createVerified().let { status ->
|
||||
Model(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.toSimple
|
||||
|
|
@ -14,7 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.toSimple
|
||||
|
|
@ -14,7 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ internal class StoreAdaptiveThrottleTest {
|
|||
val upstream = MutableSharedFlow<Set<Int>>()
|
||||
|
||||
upstream.adaptiveThrottle().test {
|
||||
|
||||
upstream.emit(setOf(1, 2))
|
||||
assertThat(awaitItem()).isEqualTo(setOf(1, 2))
|
||||
|
||||
|
|
@ -44,7 +43,6 @@ internal class StoreAdaptiveThrottleTest {
|
|||
val upstream = MutableSharedFlow<Set<Int>>()
|
||||
|
||||
upstream.adaptiveThrottle().test {
|
||||
|
||||
upstream.emit(setOf(1, 2))
|
||||
awaitItem()
|
||||
|
||||
|
|
@ -64,7 +62,6 @@ internal class StoreAdaptiveThrottleTest {
|
|||
val upstream = MutableSharedFlow<Set<Int>>()
|
||||
|
||||
upstream.adaptiveThrottle().test {
|
||||
|
||||
upstream.emit(setOf(1, 2))
|
||||
awaitItem()
|
||||
|
||||
|
|
@ -88,7 +85,6 @@ internal class StoreAdaptiveThrottleTest {
|
|||
val upstream = MutableSharedFlow<Set<Int>>()
|
||||
|
||||
upstream.adaptiveThrottle().test {
|
||||
|
||||
upstream.emit(setOf(1, 2))
|
||||
awaitItem()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -14,7 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.toDataModel
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.networks.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.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
|
||||
|
|
@ -17,7 +17,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -12,21 +12,20 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.network.NetworkStatus.Amount
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
internal class NetworkStatusFactoryTest(private val model: Model) {
|
||||
internal class NetworkStatusFactoryTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun test(model: Model) {
|
||||
val actual = runCatching {
|
||||
NetworkStatusFactory.create(
|
||||
network = model.network,
|
||||
|
|
@ -40,8 +39,9 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
.onFailure {
|
||||
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(it::class.java)
|
||||
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(it.message)
|
||||
val expectedError = model.expected.exceptionOrNull()
|
||||
Truth.assertThat(it).isInstanceOf(expectedError!!::class.java)
|
||||
Truth.assertThat(it).hasMessageThat().isEqualTo(expectedError.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,11 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
|
||||
val selectedAddressThrowable = IllegalArgumentException("Selected address must not be null")
|
||||
|
||||
val currencies = with(MockCryptoCurrencyFactory()) { setOf(ethereum, createToken(Blockchain.Ethereum)) }
|
||||
val currencies = with(MockCryptoCurrencyFactory()) {
|
||||
// token id/contractAddress aligned with the amounts supplied by
|
||||
// MockUpdateWalletManagerResultFactory.createVerifiedWith[Supplied]Token()
|
||||
setOf(ethereum, createToken(Blockchain.Ethereum, id = "token", contractAddress = "0xTokenAddress"))
|
||||
}
|
||||
|
||||
val txInfo = TxInfo(
|
||||
txHash = "erroribus",
|
||||
|
|
@ -75,8 +79,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
val updateWalletManagerResultFactory = MockUpdateWalletManagerResultFactory()
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Model> = listOf(
|
||||
fun provideTestModels(): Collection<Model> = listOf(
|
||||
// region MissedDerivation
|
||||
createSuccess(
|
||||
result = UpdateWalletManagerResult.MissedDerivation,
|
||||
|
|
@ -160,7 +163,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
type = NetworkAddress.Address.Type.Primary,
|
||||
),
|
||||
),
|
||||
amountToCreateAccount = BigDecimal.ZERO,
|
||||
amountToCreateAccount = BigDecimal.ONE,
|
||||
errorMessage = "",
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
|
|
@ -223,7 +226,10 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
currencies.last().id to setOf(),
|
||||
),
|
||||
source = StatusSource.ACTUAL,
|
||||
yieldSupplyStatuses = mapOf(),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
currencies.first().id to null,
|
||||
currencies.last().id to null,
|
||||
),
|
||||
),
|
||||
),
|
||||
createSuccess(
|
||||
|
|
@ -237,15 +243,18 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
),
|
||||
),
|
||||
amounts = mapOf(
|
||||
currencies.first().id to Amount.Loaded(BigDecimal.ONE),
|
||||
currencies.last().id to Amount.NotFound,
|
||||
currencies.first().id to Amount.NotFound,
|
||||
currencies.last().id to Amount.Loaded(BigDecimal.ONE),
|
||||
),
|
||||
pendingTransactions = mapOf(
|
||||
currencies.first().id to setOf(txInfo),
|
||||
currencies.last().id to setOf(txInfo),
|
||||
currencies.last().id to setOf(),
|
||||
),
|
||||
source = StatusSource.ACTUAL,
|
||||
yieldSupplyStatuses = mapOf(),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
currencies.first().id to null,
|
||||
currencies.last().id to null,
|
||||
),
|
||||
),
|
||||
),
|
||||
createSuccess(
|
||||
|
|
@ -259,18 +268,19 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
),
|
||||
),
|
||||
amounts = mapOf(
|
||||
currencies.first().id to Amount.Loaded(BigDecimal.ONE),
|
||||
currencies.last().id to Amount.NotFound,
|
||||
currencies.first().id to Amount.NotFound,
|
||||
currencies.last().id to Amount.Loaded(BigDecimal.ONE),
|
||||
),
|
||||
pendingTransactions = mapOf(
|
||||
currencies.first().id to setOf(txInfo),
|
||||
currencies.last().id to setOf(txInfo),
|
||||
currencies.last().id to setOf(),
|
||||
),
|
||||
source = StatusSource.ACTUAL,
|
||||
yieldSupplyStatuses = mapOf(
|
||||
currencies.first().id to YieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
currencies.first().id to null,
|
||||
currencies.last().id to YieldSupplyStatus(
|
||||
isActive = true,
|
||||
isInitialized = true,
|
||||
isAllowedToSpend = false,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.news"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -44,7 +39,6 @@ dependencies {
|
|||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.nft"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -60,5 +55,4 @@ dependencies {
|
|||
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region tests
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import io.mockk.coVerify
|
|||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.squareup.moshi.Moshi
|
||||
import androidx.datastore.core.DataStore
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultPushNotificationsRepositoryTest {
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
|
|||
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
|
||||
import com.tangem.datasource.local.converter.toEntity
|
||||
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
|
||||
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
|
||||
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
|||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -72,12 +74,13 @@ internal class DefaultOnrampRepository(
|
|||
private val currenciesStore: OnrampCurrenciesStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
moshi: Moshi,
|
||||
) : OnrampRepository {
|
||||
|
||||
private val currencyConverter = CurrencyConverter()
|
||||
private val countryConverter = CountryConverter(currencyConverter)
|
||||
private val statusConverter = StatusConverter()
|
||||
private val statusConverter = StatusConverter(moshi)
|
||||
private val paymentMethodsConverter = PaymentMethodConverter()
|
||||
private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java)
|
||||
private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java)
|
||||
|
|
@ -162,6 +165,8 @@ internal class DefaultOnrampRepository(
|
|||
)
|
||||
.getOrThrow()
|
||||
|
||||
expressHistoryDao.upsertOnramps(listOf(response.toEntity(ownerAddress = response.payoutAddress)))
|
||||
|
||||
statusConverter.convert(response)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package com.tangem.data.onramp.converters
|
||||
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.Status
|
||||
import com.tangem.domain.onramp.model.OnrampStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> {
|
||||
override fun convert(value: OnrampStatusResponse): OnrampStatus {
|
||||
internal class StatusConverter(moshi: Moshi) : Converter<OnrampItemResponse, OnrampStatus> {
|
||||
|
||||
private val responseStatusAdapter = moshi.adapter(Status::class.java)
|
||||
|
||||
override fun convert(value: OnrampItemResponse): OnrampStatus {
|
||||
return OnrampStatus(
|
||||
txId = value.txId,
|
||||
providerId = value.providerId,
|
||||
payoutAddress = value.payoutAddress,
|
||||
status = OnrampStatus.Status.valueOf(value.status.name),
|
||||
status = OnrampStatus.Status.valueOf(value.status.toResponseStatus().name),
|
||||
failReason = value.failReason,
|
||||
externalTxId = value.externalTxId,
|
||||
externalTxUrl = value.externalTxUrl,
|
||||
|
|
@ -20,11 +25,14 @@ internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> {
|
|||
fromAmount = value.fromAmount,
|
||||
toContractAddress = value.toContractAddress,
|
||||
toNetwork = value.toNetwork,
|
||||
toDecimals = value.toDecimals,
|
||||
toDecimals = value.toDecimals.toString(),
|
||||
toAmount = value.toAmount,
|
||||
toActualAmount = value.toActualAmount,
|
||||
paymentMethod = value.paymentMethod,
|
||||
countryCode = value.countryCode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.toResponseStatus(): Status = responseStatusAdapter.fromJsonValue(this)
|
||||
?: error("Unknown onramp status: $this")
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsSto
|
|||
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
|
||||
import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.onramp.repositories.*
|
||||
|
|
@ -56,6 +57,7 @@ internal object OnrampDataModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
dataSignatureVerifier: DataSignatureVerifier,
|
||||
onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
|
||||
expressHistoryDao: ExpressHistoryDao,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): OnrampRepository {
|
||||
return DefaultOnrampRepository(
|
||||
|
|
@ -71,6 +73,7 @@ internal object OnrampDataModule {
|
|||
countriesStore = countriesStore,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dataSignatureVerifier = dataSignatureVerifier,
|
||||
expressHistoryDao = expressHistoryDao,
|
||||
moshi = moshi,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,26 @@ internal class DefaultWalletPushNotificationPreferencesRepository(
|
|||
isEnabled: Boolean,
|
||||
): Either<Throwable, Unit> = Either.catch {
|
||||
val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId)
|
||||
val updated = applyCategory(current, category, isEnabled)
|
||||
val updated = current.withCategory(category, isEnabled)
|
||||
putAndCommit(userWalletId, updated)
|
||||
}
|
||||
|
||||
override suspend fun setAllPreferences(
|
||||
userWalletId: UserWalletId,
|
||||
transactionAlerts: Boolean,
|
||||
offersUpdates: Boolean,
|
||||
priceAlerts: Boolean,
|
||||
): Either<Throwable, Unit> = Either.catch {
|
||||
val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId)
|
||||
val updated = current.copy(
|
||||
transactionAlerts = current.transactionAlerts.copy(isEnabled = transactionAlerts),
|
||||
offersUpdates = current.offersUpdates.copy(isEnabled = offersUpdates),
|
||||
priceAlerts = current.priceAlerts.copy(isEnabled = priceAlerts),
|
||||
)
|
||||
putAndCommit(userWalletId, updated)
|
||||
}
|
||||
|
||||
private suspend fun putAndCommit(userWalletId: UserWalletId, updated: WalletPushNotificationPreferences) {
|
||||
withContext(dispatchers.io) {
|
||||
// TODO: uncomment when api is ready
|
||||
// tangemTechApi.updatePushNotificationPreferences(
|
||||
|
|
@ -80,22 +99,6 @@ internal class DefaultWalletPushNotificationPreferencesRepository(
|
|||
cache.update(default = emptyMap()) { it + (userWalletId.stringValue to updated) }
|
||||
}
|
||||
|
||||
private fun applyCategory(
|
||||
current: WalletPushNotificationPreferences,
|
||||
category: PushNotificationCategory,
|
||||
isEnabled: Boolean,
|
||||
): WalletPushNotificationPreferences = when (category) {
|
||||
PushNotificationCategory.TransactionAlerts -> current.copy(
|
||||
transactionAlerts = current.transactionAlerts.copy(isEnabled = isEnabled),
|
||||
)
|
||||
PushNotificationCategory.OffersUpdates -> current.copy(
|
||||
offersUpdates = current.offersUpdates.copy(isEnabled = isEnabled),
|
||||
)
|
||||
PushNotificationCategory.PriceAlerts -> current.copy(
|
||||
priceAlerts = current.priceAlerts.copy(isEnabled = isEnabled),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO remove when api is ready, use api methods to load real settings
|
||||
private suspend fun loadDefaults(userWalletId: UserWalletId): WalletPushNotificationPreferences {
|
||||
val areTransactionAlertsEnabled = appPreferencesStore
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import io.mockk.coEvery
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultWalletPushNotificationPreferencesRepositoryTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class Bip321PaymentUriParserTest {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.QrResult
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultQrScanningEventsRepositoryTest {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class Eip681PaymentUriParserTest {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QrContentClassifierTest {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SolanaPaymentUriParserTest {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TronPaymentUriParserTest {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.quotes"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -47,7 +42,6 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Tests
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ internal class QuoteStatusConverterTest {
|
|||
priceChange24h = BigDecimal.ONE,
|
||||
priceChange1w = null,
|
||||
priceChange30d = null,
|
||||
priceUsd = BigDecimal.ONE
|
||||
priceUsd = BigDecimal.ONE,
|
||||
),
|
||||
),
|
||||
expected = QuoteStatus(
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
),
|
||||
)
|
||||
|
||||
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H, QuotesFetcher.Field.PRICE_USD)
|
||||
val fields =
|
||||
setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H, QuotesFetcher.Field.PRICE_USD)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.quotes.multi
|
|||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.test.core.getEmittedValues
|
|||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
package com.tangem.data.quotes.single
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.data.quotes.store.QuotesStatusesStore
|
||||
import com.tangem.domain.core.flow.FlowProducerTools
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
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
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultSingleQuoteStatusProducerTest {
|
||||
|
||||
private val params = SingleQuoteStatusProducer.Params(
|
||||
|
|
@ -27,14 +34,22 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
)
|
||||
|
||||
private val quotesStore = mockk<QuotesStatusesStore>()
|
||||
private val flowProducerTools: FlowProducerTools = mockk()
|
||||
|
||||
private val producer = DefaultSingleQuoteStatusProducer(
|
||||
params = params,
|
||||
quotesStatusesStore = quotesStore,
|
||||
flowProducerTools = flowProducerTools,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
private fun TestScope.createProducer(): DefaultSingleQuoteStatusProducer {
|
||||
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
return DefaultSingleQuoteStatusProducer(
|
||||
params = params,
|
||||
quotesStatusesStore = quotesStore,
|
||||
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 network from params`() = runTest {
|
||||
|
|
@ -48,7 +63,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
|
||||
every { quotesStore.get() } returns storeQuote
|
||||
|
||||
val actual = producer.produce()
|
||||
val actual = createProducer().produce()
|
||||
|
||||
verify { quotesStore.get() }
|
||||
|
||||
|
|
@ -64,35 +79,29 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
|
||||
every { quotesStore.get() } returns storeQuote
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { quotesStore.get() }
|
||||
|
||||
// first emit
|
||||
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
storeQuote.emit(value = setOf(status))
|
||||
actual.test {
|
||||
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
storeQuote.emit(value = setOf(status))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
val updatedStatus = QuoteStatus(
|
||||
rawCurrencyId = params.rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
)
|
||||
storeQuote.emit(value = setOf(updatedStatus))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(updatedStatus)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(status))
|
||||
|
||||
// second emit
|
||||
val updatedStatus = QuoteStatus(
|
||||
rawCurrencyId = params.rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
source = StatusSource.ACTUAL,
|
||||
),
|
||||
)
|
||||
storeQuote.emit(value = setOf(updatedStatus))
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -101,26 +110,21 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
|
||||
every { quotesStore.get() } returns storeQuote
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { quotesStore.get() }
|
||||
|
||||
// first emit
|
||||
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
storeQuote.emit(value = setOf(status))
|
||||
actual.test {
|
||||
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
storeQuote.emit(value = setOf(status))
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
// same status again -> filtered out by distinctUntilChanged
|
||||
storeQuote.emit(value = setOf(status))
|
||||
expectNoEvents()
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(status))
|
||||
|
||||
// second emit
|
||||
storeQuote.emit(value = setOf(status))
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -148,21 +152,22 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
|
||||
every { quotesStore.get() } returns storeQuote
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { quotesStore.get() }
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
actual.test {
|
||||
val fallbackStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
Truth.assertThat(awaitItem()).isEqualTo(fallbackStatus)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
val fallbackStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
|
||||
Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus))
|
||||
innerFlow.value = true
|
||||
advanceTimeBy(delayTimeMillis = 2001)
|
||||
runCurrent()
|
||||
|
||||
innerFlow.emit(value = true)
|
||||
Truth.assertThat(awaitItem()).isEqualTo(status)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
Truth.assertThat(values2.size).isEqualTo(1)
|
||||
Truth.assertThat(values2).isEqualTo(listOf(status))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -175,12 +180,14 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
|
||||
every { quotesStore.get() } returns storeFlow
|
||||
|
||||
val actual = producer.produceWithFallback()
|
||||
val actual = createProducer().produceWithFallback()
|
||||
|
||||
verify { quotesStore.get() }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(0)
|
||||
actual.test {
|
||||
// params currency (BTC) is not in the store -> nothing is emitted
|
||||
expectNoEvents()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.data.quotes.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.quote.MockQuoteResponseFactory
|
||||
import com.tangem.common.test.data.quote.toDomain
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.tangem.data.quotes.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.quote.MockQuoteResponseFactory
|
||||
import com.tangem.common.test.data.quote.toDomain
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.settings"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -34,7 +29,6 @@ dependencies {
|
|||
|
||||
// region Test
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.swap"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -46,6 +41,9 @@ dependencies {
|
|||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.configToggles)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
@ -62,7 +60,6 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(tangemDeps.card.core)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import arrow.core.toOption
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.swap.converter.SwapDataConverter
|
||||
import com.tangem.data.swap.converter.SwapStatusConverter
|
||||
import com.tangem.data.swap.converter.TokenInfoConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
|
|
@ -57,12 +58,12 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
) : SwapRepositoryV2 {
|
||||
|
||||
private val swapDataConverter = SwapDataConverter()
|
||||
private val tokenInfoConverter = TokenInfoConverter()
|
||||
private val exchangeStatusConverter = SwapStatusConverter()
|
||||
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
|
||||
|
||||
override suspend fun getPairs(
|
||||
|
|
@ -401,22 +402,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): SwapStatusModel =
|
||||
withContext(coroutineDispatcher.io) {
|
||||
exchangeStatusConverter.convert(
|
||||
tangemExpressApi
|
||||
.getExchangeStatus(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
refCode = ExpressUtils.getRefCode(
|
||||
userWallet = userWallet,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
),
|
||||
txId = txId,
|
||||
)
|
||||
.getOrThrow(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun CoroutineScope.getPairsInternal(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: CryptoCurrency,
|
||||
|
|
@ -544,16 +529,21 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
|
||||
}
|
||||
|
||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) =
|
||||
filter { provider ->
|
||||
// !!!WARNING!!! Filter out dex provider if yield supply is active
|
||||
val yieldSupplyStatus = cryptoCurrencyStatus?.value?.yieldSupplyStatus
|
||||
if (yieldSupplyStatus != null && yieldSupplyStatus.isActive) {
|
||||
provider.type == ExpressProviderType.CEX
|
||||
} else {
|
||||
true
|
||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
): List<ExpressProvider> {
|
||||
return if (featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED)) {
|
||||
this
|
||||
} else {
|
||||
filter { provider ->
|
||||
if (cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true) {
|
||||
provider.type == ExpressProviderType.CEX
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MEMO_RESTRICTED_NETWORKS = setOf(
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.data.swap.converter
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse
|
||||
import com.tangem.domain.swap.models.SwapStatus
|
||||
import com.tangem.domain.swap.models.SwapStatusModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SwapStatusConverter : Converter<ExchangeStatusResponse, SwapStatusModel> {
|
||||
override fun convert(value: ExchangeStatusResponse): SwapStatusModel {
|
||||
return SwapStatusModel(
|
||||
providerId = value.providerId,
|
||||
status = SwapStatus.entries.firstOrNull {
|
||||
it.name.lowercase() == value.status.name.lowercase()
|
||||
},
|
||||
txId = value.externalTxId,
|
||||
txExternalUrl = value.externalTxUrl,
|
||||
txExternalId = value.externalTxId,
|
||||
refundNetwork = value.refundNetwork,
|
||||
refundContractAddress = value.refundContractAddress,
|
||||
createdAt = value.createdAt,
|
||||
averageDuration = value.averageDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapErrorResolver
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.swap.SwapTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -49,6 +50,7 @@ internal object SwapDataModule {
|
|||
dataSignatureVerifier: DataSignatureVerifier,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): SwapRepositoryV2 {
|
||||
return DefaultSwapRepositoryV2(
|
||||
|
|
@ -60,6 +62,7 @@ internal object SwapDataModule {
|
|||
moshi = moshi,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
featureTogglesManager = featureTogglesManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import com.tangem.domain.swap.models.SwapAmountType
|
|||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapStatus
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -43,6 +45,9 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk()
|
||||
private val moshi: Moshi = Moshi.Builder().build()
|
||||
private val featureTogglesManager: FeatureTogglesManager = mockk {
|
||||
every { isFeatureEnabled(any()) } returns false
|
||||
}
|
||||
|
||||
private val repository = DefaultSwapRepositoryV2(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
|
|
@ -52,6 +57,7 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
dataSignatureVerifier = dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
featureTogglesManager = featureTogglesManager,
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
|
|
@ -64,7 +70,9 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher,
|
||||
featureTogglesManager,
|
||||
)
|
||||
every { featureTogglesManager.isFeatureEnabled(any()) } returns false
|
||||
}
|
||||
|
||||
// region getPairs(SwapCurrencyStatus, SwapCurrencyStatus)
|
||||
|
|
@ -441,35 +449,6 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
|
||||
// endregion
|
||||
|
||||
// region getExchangeStatus
|
||||
|
||||
@Test
|
||||
fun `getExchangeStatus returns converted status model`() = runTest {
|
||||
// Arrange
|
||||
val statusResponse = ExchangeStatusResponse(
|
||||
providerId = PROVIDER_ID,
|
||||
status = ExchangeStatus.Finished,
|
||||
externalTxId = "ext-tx-1",
|
||||
externalTxUrl = "https://example.com/tx/1",
|
||||
error = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeStatus(any(), any(), any())
|
||||
} returns ApiResponse.Success(statusResponse)
|
||||
|
||||
// Act
|
||||
val result = repository.getExchangeStatus(userWallet = userWallet, txId = "tx-123")
|
||||
|
||||
// Assert
|
||||
assertThat(result.providerId).isEqualTo(PROVIDER_ID)
|
||||
assertThat(result.status).isEqualTo(SwapStatus.Finished)
|
||||
assertThat(result.txId).isEqualTo("ext-tx-1")
|
||||
assertThat(result.txExternalUrl).isEqualTo("https://example.com/tx/1")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region swapTransactionSent
|
||||
|
||||
@Test
|
||||
|
|
@ -511,8 +490,9 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
// region filterYieldSupplyProvider
|
||||
|
||||
@Test
|
||||
fun `getPairs filters out DEX providers when yield supply is active`() = runTest {
|
||||
fun `getPairs filters out DEX providers when yield supply is active and flag is off`() = runTest {
|
||||
// Arrange
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns false
|
||||
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
|
|
@ -558,6 +538,54 @@ internal class DefaultSwapRepositoryV2Test {
|
|||
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs keeps DEX providers when yield supply is active and flag is on`() = runTest {
|
||||
// Arrange
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED) } returns true
|
||||
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(
|
||||
SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(dexProvider, cexProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert — both providers should remain
|
||||
assertThat(result).hasSize(2)
|
||||
val providers = result.first().providers
|
||||
assertThat(providers).hasSize(2)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getSwapData
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.tokens"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Project - Data
|
||||
|
|
@ -50,7 +45,7 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Project - Features API
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.send.api)
|
||||
// endregion
|
||||
|
||||
// region Tangem SDKs
|
||||
|
|
@ -78,7 +73,6 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Tests
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -9,11 +9,6 @@ plugins {
|
|||
android {
|
||||
namespace = "com.tangem.data.transaction"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Tangem SDKs */
|
||||
|
|
@ -39,7 +34,7 @@ dependencies {
|
|||
implementation(projects.domain.demo)
|
||||
|
||||
/** Api */
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.send.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -50,7 +45,6 @@ dependencies {
|
|||
/** tests */
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ internal class DefaultFeeErrorResolver : FeeErrorResolver {
|
|||
is BlockchainSdkError.Sui.OneSuiRequired -> {
|
||||
GetFeeError.BlockchainErrors.SuiOneCoinRequired
|
||||
}
|
||||
is BlockchainSdkError.Ethereum.EstimateOverrideError -> {
|
||||
GetFeeError.EstimateOverrideError(
|
||||
blockchain = throwable.blockchain,
|
||||
tokenSymbol = throwable.tokenSymbol,
|
||||
rpcProvider = throwable.rpcProvider,
|
||||
error = throwable.underlyingError,
|
||||
)
|
||||
}
|
||||
else -> GetFeeError.DataError(throwable)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.data.transaction.error
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [DefaultFeeErrorResolver] — the [Throwable] -> [GetFeeError] resolver. Mirrors the
|
||||
* mapping matrix of `ErrorsMapper.mapToFeeError` but driven through `resolve(throwable)`.
|
||||
*
|
||||
* Focuses on the [REDACTED_TASK_KEY] addition: [BlockchainSdkError.Ethereum.EstimateOverrideError] must be
|
||||
* resolved to [GetFeeError.EstimateOverrideError] field-by-field; representative other chains map
|
||||
* to their dedicated [GetFeeError.BlockchainErrors]; everything else falls through to
|
||||
* [GetFeeError.DataError].
|
||||
*/
|
||||
internal class DefaultFeeErrorResolverTest {
|
||||
|
||||
private val resolver = DefaultFeeErrorResolver()
|
||||
|
||||
@Test
|
||||
fun `GIVEN EstimateOverrideError THEN resolves to GetFeeError EstimateOverrideError field by field`() {
|
||||
val sdkError = BlockchainSdkError.Ethereum.EstimateOverrideError(
|
||||
blockchain = "ethereum",
|
||||
tokenSymbol = "USDT",
|
||||
rpcProvider = "infura",
|
||||
underlyingError = "execution reverted",
|
||||
)
|
||||
|
||||
val result = resolver.resolve(sdkError)
|
||||
|
||||
assertThat(result).isInstanceOf(GetFeeError.EstimateOverrideError::class.java)
|
||||
val mapped = result as GetFeeError.EstimateOverrideError
|
||||
assertThat(mapped.blockchain).isEqualTo("ethereum")
|
||||
assertThat(mapped.tokenSymbol).isEqualTo("USDT")
|
||||
assertThat(mapped.rpcProvider).isEqualTo("infura")
|
||||
assertThat(mapped.error).isEqualTo("execution reverted")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN TronActivationError THEN resolves to TronActivationError`() {
|
||||
// AccountActivationError is a class taking an int code, not an object.
|
||||
val result = resolver.resolve(BlockchainSdkError.Tron.AccountActivationError(code = 0))
|
||||
|
||||
assertThat(result).isEqualTo(GetFeeError.BlockchainErrors.TronActivationError)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN KaspaZeroUtxoError THEN resolves to KaspaZeroUtxo`() {
|
||||
val result = resolver.resolve(BlockchainSdkError.Kaspa.ZeroUtxoError)
|
||||
|
||||
assertThat(result).isEqualTo(GetFeeError.BlockchainErrors.KaspaZeroUtxo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN SuiOneSuiRequired THEN resolves to SuiOneCoinRequired`() {
|
||||
val result = resolver.resolve(BlockchainSdkError.Sui.OneSuiRequired)
|
||||
|
||||
assertThat(result).isEqualTo(GetFeeError.BlockchainErrors.SuiOneCoinRequired)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown error THEN resolves to DataError preserving the throwable`() {
|
||||
val sdkError = BlockchainSdkError.CustomError("boom")
|
||||
|
||||
val result = resolver.resolve(sdkError)
|
||||
|
||||
assertThat(result).isInstanceOf(GetFeeError.DataError::class.java)
|
||||
assertThat((result as GetFeeError.DataError).cause).isEqualTo(sdkError)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,17 @@ android {
|
|||
namespace = "com.tangem.data.txhistory"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.data.common)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.core.analytics)
|
||||
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.common)
|
||||
|
|
@ -24,6 +29,9 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
|
|
@ -34,4 +42,11 @@ dependencies {
|
|||
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
// region Test
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.test.mock)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,51 +1,35 @@
|
|||
package com.tangem.data.txhistory.di
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.fetcher.DefaultAppTxHistoryFetcher
|
||||
import com.tangem.data.txhistory.fetcher.DefaultTxHistoryFetcherUtils
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils
|
||||
import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository
|
||||
import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.txhistory.fetcher.AppTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TxHistoryDataModule {
|
||||
internal interface TxHistoryDataModule {
|
||||
|
||||
@Provides
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideTxHistoryRepository(
|
||||
cacheRegistry: CacheRegistry,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
txHistoryItemsStore: TxHistoryItemsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): TxHistoryRepository = DefaultTxHistoryRepository(
|
||||
cacheRegistry = cacheRegistry,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
fun provideTxHistoryRepository(default: DefaultTxHistoryRepository): TxHistoryRepository
|
||||
|
||||
@Provides
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideTxHistoryRepositoryV2(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
txHistoryItemsStore: TxHistoryItemsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
): TxHistoryRepositoryV2 = RefactoredTxHistoryRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
)
|
||||
fun provideTxHistoryRepositoryV2(default: RefactoredTxHistoryRepository): TxHistoryRepositoryV2
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideAppTxHistoryFetcher(default: DefaultAppTxHistoryFetcher): AppTxHistoryFetcher
|
||||
|
||||
@Binds
|
||||
fun provideTxHistoryFetcherUtils(default: DefaultTxHistoryFetcherUtils): TxHistoryFetcherUtils
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.txhistory.fetcher.AccountTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.ExpressTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class DefaultAccountTxHistoryFetcher @AssistedInject constructor(
|
||||
@Assisted override val accountId: AccountId,
|
||||
private val utils: TxHistoryFetcherUtils,
|
||||
private val singleAccountSupplier: SingleAccountSupplier,
|
||||
private val paymentAccountCurrency: GetPaymentAccountCryptoCurrencyStatusUseCase,
|
||||
private val expressFetcherFactory: DefaultExpressTxHistoryFetcher.Factory,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) : AccountTxHistoryFetcher, TxHistoryFetcherUtils by utils {
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val expressFetchers = ConcurrentHashMap<String, ExpressTxHistoryFetcher>()
|
||||
|
||||
init {
|
||||
defaultLaunchIn(buildFlow())
|
||||
}
|
||||
|
||||
override suspend fun invoke(params: TxHistoryFetchTrigger) {
|
||||
sendTrigger(params)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cancelScope()
|
||||
expressFetchers.forEach { (_, fetcher) -> fetcher.close() }
|
||||
expressFetchers.clear()
|
||||
}
|
||||
|
||||
private fun buildFlow(): Flow<Unit> = channelFlow {
|
||||
val accountFlow = singleAccountSupplier(accountId).stateIn(this)
|
||||
|
||||
when (val account = accountFlow.value) {
|
||||
is Account.CryptoPortfolio -> {
|
||||
account.getExpressKeys().createExpressFetcher()
|
||||
accountFlow
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.controlFetchersForCryptoAccount()
|
||||
.launchIn(this)
|
||||
}
|
||||
is Account.Payment -> {
|
||||
paymentAccountCurrency.invokeSync(walletId)
|
||||
.getOrNull()
|
||||
?.controlFetchersForPaymentAccount()
|
||||
controlFetchersForPaymentAccount()
|
||||
.launchIn(this)
|
||||
}
|
||||
// Virtual account tx-history isn't wired yet (separate task) — no express fetchers for now.
|
||||
is Account.Virtual -> Unit
|
||||
}
|
||||
|
||||
receiveTrigger().onEach { trigger ->
|
||||
when (trigger) {
|
||||
is TxHistoryFetchTrigger.TokenDetailsOpen -> {
|
||||
val addressKey = getAddress(trigger.walletId, trigger.currency) ?: return@onEach
|
||||
expressFetchers[addressKey]?.invoke(trigger)
|
||||
}
|
||||
is TxHistoryFetchTrigger.TokenDetailsPTR -> {
|
||||
val addressKey = getAddress(trigger.walletId, trigger.currency) ?: return@onEach
|
||||
expressFetchers[addressKey]?.invoke(trigger)
|
||||
}
|
||||
}
|
||||
}.collect {}
|
||||
}
|
||||
|
||||
private fun controlFetchersForPaymentAccount(): Flow<Unit> {
|
||||
return paymentAccountCurrency(walletId)
|
||||
.map { pair -> pair.controlFetchersForPaymentAccount() }
|
||||
}
|
||||
|
||||
private suspend fun Pair<AccountStatus.Payment, CryptoCurrencyStatus>?.controlFetchersForPaymentAccount() {
|
||||
val (_, paymentCurrency) = this ?: return
|
||||
val paymentNetwork = paymentCurrency.currency.network
|
||||
val address = getAddress(walletId, paymentCurrency.currency)
|
||||
if (paymentNetwork.isSupportExpressTxHistory() && !address.isNullOrBlank()) {
|
||||
getOrPutExpressFetcher(address)
|
||||
} else {
|
||||
// single currency for payment account, so we can close all(one)
|
||||
expressFetchers.forEach { (_, fetcher) -> fetcher.close() }
|
||||
expressFetchers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<Account.CryptoPortfolio>.controlFetchersForCryptoAccount(): Flow<Unit> {
|
||||
return map { account ->
|
||||
val newExpressKeys = account.getExpressKeys()
|
||||
val previousExpressKeys = expressFetchers.keys
|
||||
val removed = previousExpressKeys - newExpressKeys
|
||||
newExpressKeys.createExpressFetcher()
|
||||
removed.forEach { address -> expressFetchers.remove(address)?.close() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.createExpressFetcher() = this.forEach { address -> getOrPutExpressFetcher(address) }
|
||||
|
||||
private suspend fun Account.CryptoPortfolio.getExpressKeys(): Set<String> {
|
||||
val currencies = this.cryptoCurrencies
|
||||
val onlyCoins = currencies.filterIsInstance<CryptoCurrency.Coin>()
|
||||
val networks = onlyCoins.map { coin -> coin.network }
|
||||
val newExpressKeys = networks
|
||||
.filter { net -> net.isSupportExpressTxHistory() }
|
||||
.mapNotNull { net -> getAddress(walletId, net) }
|
||||
.toSet()
|
||||
return newExpressKeys
|
||||
}
|
||||
|
||||
@Suppress("FunctionOnlyReturningConstant") // todo txhistory check
|
||||
private fun Network.isSupportExpressTxHistory(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun getAddress(userWalletId: UserWalletId, currencies: CryptoCurrency): String? =
|
||||
getAddress(userWalletId, currencies.network)
|
||||
|
||||
private suspend fun getAddress(userWalletId: UserWalletId, network: Network): String? =
|
||||
walletManagersFacade.getDefaultAddress(userWalletId, network)
|
||||
|
||||
private fun getOrPutExpressFetcher(address: String): ExpressTxHistoryFetcher {
|
||||
return expressFetchers.computeIfAbsent(address) { expressFetcherFactory.create(address, accountId) }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
internal interface Factory {
|
||||
fun create(accountId: AccountId): DefaultAccountTxHistoryFetcher
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.txhistory.fetcher.AppTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.txhistory.fetcher.WalletTxHistoryFetcher
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAppTxHistoryFetcher @Inject constructor(
|
||||
private val utils: TxHistoryFetcherUtils,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val selectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val walletTxHistoryFetcherFactory: DefaultWalletTxHistoryFetcher.Factory,
|
||||
) : AppTxHistoryFetcher, TxHistoryFetcherUtils by utils {
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val fetchers = ConcurrentHashMap<UserWalletId, WalletTxHistoryFetcher>()
|
||||
|
||||
init {
|
||||
defaultLaunchIn(buildFlow())
|
||||
}
|
||||
|
||||
override suspend fun invoke(params: TxHistoryFetchTrigger) {
|
||||
sendTrigger(params)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cancelScope()
|
||||
fetchers.forEach { (_, fetcher) -> fetcher.close() }
|
||||
fetchers.clear()
|
||||
}
|
||||
|
||||
private fun buildFlow(): Flow<Unit> = channelFlow {
|
||||
val walletsFlow: StateFlow<Map<UserWalletId, UserWallet>> = getWalletsUseCase
|
||||
.invokeAsMap(isOnlyMultiCurrency = true, filterLocked = true)
|
||||
.stateIn(this)
|
||||
|
||||
walletsFlow.value.keys.createForNewWallets()
|
||||
|
||||
selectedWalletUseCase.selectedFlow()
|
||||
.filter { wallet -> wallet.isMultiCurrency }
|
||||
// todo txhistory some init trigger?
|
||||
.onEach { }
|
||||
.launchIn(this)
|
||||
|
||||
walletsFlow
|
||||
.map { map -> map.keys }
|
||||
.distinctUntilChanged()
|
||||
// todo txhistory create for all or lazy?
|
||||
.createForNewWallets()
|
||||
.closeForRemovedWallets()
|
||||
.launchIn(this)
|
||||
|
||||
receiveTrigger()
|
||||
.onEach { trigger ->
|
||||
when (trigger) {
|
||||
is TxHistoryFetchTrigger.TokenDetailsOpen -> fetchers[trigger.walletId]?.invoke(trigger)
|
||||
is TxHistoryFetchTrigger.TokenDetailsPTR -> fetchers[trigger.walletId]?.invoke(trigger)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
private fun Flow<Set<UserWalletId>>.createForNewWallets() = onEach { ids -> ids.createForNewWallets() }
|
||||
|
||||
private fun Set<UserWalletId>.createForNewWallets() = this.forEach { walletId -> getOrPutFetcher(walletId) }
|
||||
|
||||
private fun Flow<Set<UserWalletId>>.closeForRemovedWallets() = runningReduce { previousIds, newIds ->
|
||||
val removedWallets = previousIds.subtract(newIds)
|
||||
removedWallets.forEach { walletId -> fetchers.remove(walletId)?.close() }
|
||||
newIds
|
||||
}
|
||||
|
||||
private fun getOrPutFetcher(id: UserWalletId): WalletTxHistoryFetcher {
|
||||
return fetchers.computeIfAbsent(id) { createFetcher(id) }
|
||||
}
|
||||
|
||||
private fun createFetcher(id: UserWalletId): WalletTxHistoryFetcher {
|
||||
return walletTxHistoryFetcherFactory.create(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTriggerInstance
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.retryThreeTimes
|
||||
import com.tangem.data.txhistory.repository.ExpressHistoryRepository
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.fetcher.ExpressTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryExpressTrigger
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultExpressTxHistoryFetcher @AssistedInject constructor(
|
||||
@Assisted override val address: String,
|
||||
@Assisted private val accountId: AccountId,
|
||||
private val utils: TxHistoryFetcherUtils,
|
||||
private val expressSyncStateDao: ExpressSyncStateDao,
|
||||
private val expressHistoryRepository: ExpressHistoryRepository,
|
||||
) : ExpressTxHistoryFetcher, TxHistoryFetcherUtils by utils {
|
||||
|
||||
private val userWalletId: UserWalletId get() = accountId.userWalletId
|
||||
|
||||
private var exchangeInitialPaginationJob: Job? = null
|
||||
private var exchangeDeltaPaginationJob: Job? = null
|
||||
|
||||
private var onrampInitialPaginationJob: Job? = null
|
||||
private var onrampDeltaPaginationJob: Job? = null
|
||||
|
||||
init {
|
||||
val receiveFlow = receiveTriggerInstance<TxHistoryExpressTrigger>()
|
||||
.onEach { trigger ->
|
||||
when (trigger) {
|
||||
is TxHistoryFetchTrigger.TokenDetailsOpen,
|
||||
is TxHistoryFetchTrigger.TokenDetailsPTR,
|
||||
-> {
|
||||
fetchExchange()
|
||||
fetchOnramp()
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultLaunchIn(receiveFlow)
|
||||
}
|
||||
|
||||
override suspend fun invoke(params: TxHistoryExpressTrigger) {
|
||||
utils.sendTrigger(params)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cancelScope()
|
||||
}
|
||||
|
||||
private fun fetchExchange() {
|
||||
if (exchangeDeltaPaginationJob?.isActive == true) return
|
||||
exchangeDeltaPaginationJob = fetcherScope.launch {
|
||||
val isFirstFetch = expressSyncState() == null
|
||||
|
||||
if (isFirstFetch) {
|
||||
flow { emit(expressHistoryRepository.fetchExchangeHistory(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return@launch
|
||||
}
|
||||
|
||||
if (exchangeInitialPaginationJob?.isActive != true) {
|
||||
exchangeInitialPaginationJob = launch { expressInitialPagination() }
|
||||
}
|
||||
|
||||
expressDeltaPagination()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun expressInitialPagination() {
|
||||
if (expressSyncState()?.isInitialCompleted == true) return
|
||||
var hasMore = true
|
||||
while (hasMore) {
|
||||
val pageResult: ExchangeHistoryResponse =
|
||||
flow { emit(expressHistoryRepository.fetchExchangeHistory(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return
|
||||
hasMore = pageResult.pagination.hasMore
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun expressDeltaPagination() {
|
||||
var hasMore = true
|
||||
while (hasMore) {
|
||||
val pageResult: ExchangeHistoryDeltaResponse =
|
||||
flow { emit(expressHistoryRepository.fetchExchangeHistoryDelta(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return
|
||||
hasMore = pageResult.pagination.hasMore
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchOnramp() {
|
||||
if (onrampDeltaPaginationJob?.isActive == true) return
|
||||
onrampDeltaPaginationJob = fetcherScope.launch {
|
||||
val isFirstFetch = onrampSyncState() == null
|
||||
|
||||
if (isFirstFetch) {
|
||||
flow { emit(expressHistoryRepository.fetchOnrampHistory(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return@launch
|
||||
}
|
||||
|
||||
if (onrampInitialPaginationJob?.isActive != true) {
|
||||
onrampInitialPaginationJob = launch { onrampInitialPagination() }
|
||||
}
|
||||
|
||||
onrampDeltaPagination()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onrampInitialPagination() {
|
||||
if (onrampSyncState()?.isInitialCompleted == true) return
|
||||
var hasMore = true
|
||||
while (hasMore) {
|
||||
val pageResult: OnrampHistoryResponse =
|
||||
flow { emit(expressHistoryRepository.fetchOnrampHistory(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return
|
||||
hasMore = pageResult.pagination.hasMore
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onrampDeltaPagination() {
|
||||
var hasMore = true
|
||||
while (hasMore) {
|
||||
val pageResult: OnrampHistoryDeltaResponse =
|
||||
flow { emit(expressHistoryRepository.fetchOnrampHistoryDelta(address, userWalletId)) }
|
||||
.retryThreeTimes()
|
||||
.firstOrNull() ?: return
|
||||
hasMore = pageResult.pagination.hasMore
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun expressSyncState(): ExpressSyncStateEntity? = expressSyncStateDao
|
||||
.observe(ExpressSyncStateEntity.Type.EXCHANGE.name, address)
|
||||
.first()
|
||||
|
||||
private suspend fun onrampSyncState(): ExpressSyncStateEntity? = expressSyncStateDao
|
||||
.observe(ExpressSyncStateEntity.Type.ONRAMP.name, address)
|
||||
.first()
|
||||
|
||||
@AssistedFactory
|
||||
internal interface Factory {
|
||||
fun create(address: String, accountId: AccountId): DefaultExpressTxHistoryFetcher
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.cancelScope
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.defaultLaunchIn
|
||||
import com.tangem.data.txhistory.fetcher.TxHistoryFetcherUtils.Companion.receiveTrigger
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyOperations.getAccountCryptoCurrency
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.fetcher.AccountTxHistoryFetcher
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.txhistory.fetcher.WalletTxHistoryFetcher
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class DefaultWalletTxHistoryFetcher @AssistedInject constructor(
|
||||
@Assisted override val walletId: UserWalletId,
|
||||
private val utils: TxHistoryFetcherUtils,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val accountTxHistoryFetcher: DefaultAccountTxHistoryFetcher.Factory,
|
||||
) : WalletTxHistoryFetcher, TxHistoryFetcherUtils by utils {
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val fetchers = ConcurrentHashMap<AccountId, AccountTxHistoryFetcher>()
|
||||
|
||||
init {
|
||||
defaultLaunchIn(buildFlow())
|
||||
}
|
||||
|
||||
override suspend fun invoke(params: TxHistoryFetchTrigger) {
|
||||
sendTrigger(params)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cancelScope()
|
||||
fetchers.forEach { (_, fetcher) -> fetcher.close() }
|
||||
fetchers.clear()
|
||||
}
|
||||
|
||||
private fun buildFlow(): Flow<Unit> = channelFlow {
|
||||
val accountListFlow = singleAccountListSupplier(walletId)
|
||||
.stateIn(this)
|
||||
|
||||
fun accountList(): AccountList = accountListFlow.value
|
||||
accountList().accounts
|
||||
.mapTo(mutableSetOf()) { it.accountId }
|
||||
.createForNewAccounts()
|
||||
|
||||
accountListFlow
|
||||
.map { accountList -> accountList.accounts.mapTo(mutableSetOf()) { account -> account.accountId } }
|
||||
.distinctUntilChanged()
|
||||
.createForNewAccounts()
|
||||
.closeForRemovedAccounts()
|
||||
.launchIn(this)
|
||||
|
||||
receiveTrigger()
|
||||
.onEach { trigger ->
|
||||
when (trigger) {
|
||||
is TxHistoryFetchTrigger.TokenDetailsOpen -> accountList()
|
||||
.findFetcher(trigger.currency)?.invoke(trigger)
|
||||
is TxHistoryFetchTrigger.TokenDetailsPTR -> accountList()
|
||||
.findFetcher(trigger.currency)?.invoke(trigger)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
private fun Flow<Set<AccountId>>.createForNewAccounts() = onEach { ids -> ids.createForNewAccounts() }
|
||||
|
||||
private fun Set<AccountId>.createForNewAccounts() = this.forEach { id -> getOrPutFetcher(id) }
|
||||
|
||||
private fun Flow<Set<AccountId>>.closeForRemovedAccounts() = runningReduce { previousIds, newIds ->
|
||||
val removedWallets = previousIds.subtract(newIds)
|
||||
removedWallets.forEach { walletId -> fetchers.remove(walletId)?.close() }
|
||||
newIds
|
||||
}
|
||||
|
||||
private fun AccountList.findFetcher(currency: CryptoCurrency): AccountTxHistoryFetcher? = this
|
||||
.getAccountCryptoCurrency(currency)
|
||||
.getOrNull()
|
||||
?.account
|
||||
?.let { account -> fetchers[account.accountId] }
|
||||
|
||||
private fun getOrPutFetcher(id: AccountId): AccountTxHistoryFetcher {
|
||||
return fetchers.computeIfAbsent(id) { createFetcher(id) }
|
||||
}
|
||||
|
||||
private fun createFetcher(id: AccountId): AccountTxHistoryFetcher {
|
||||
return accountTxHistoryFetcher.create(id)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
internal interface Factory {
|
||||
fun create(walletId: UserWalletId): DefaultWalletTxHistoryFetcher
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TX_HISTORY_TAG = "TxHistory"
|
||||
|
||||
internal interface TxHistoryFetcherUtils {
|
||||
|
||||
val triggersBuffer: Channel<TxHistoryFetchTrigger>
|
||||
|
||||
val fetcherScope: CoroutineScope
|
||||
val analyticsEventHandler: AnalyticsEventHandler
|
||||
val analyticsExceptionHandler: AnalyticsExceptionHandler
|
||||
|
||||
suspend fun sendTrigger(trigger: TxHistoryFetchTrigger)
|
||||
|
||||
companion object {
|
||||
|
||||
fun TxHistoryFetcherUtils.cancelScope() = fetcherScope.cancel()
|
||||
|
||||
fun <T> TxHistoryFetcherUtils.defaultLaunchIn(flow: Flow<T>) = flow
|
||||
.retry { error ->
|
||||
logError(error)
|
||||
true
|
||||
}
|
||||
.launchIn(fetcherScope)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun <T> Flow<T>.retryThreeTimes() = retry(3) { error ->
|
||||
logError(error)
|
||||
delay(1000)
|
||||
true
|
||||
}.catch { e -> logError(e) }
|
||||
|
||||
fun TxHistoryFetcherUtils.receiveTrigger(): Flow<TxHistoryFetchTrigger> {
|
||||
return triggersBuffer.receiveAsFlow()
|
||||
}
|
||||
|
||||
inline fun <reified R> TxHistoryFetcherUtils.receiveTriggerInstance(): Flow<R> {
|
||||
return receiveTrigger().filterIsInstance<R>()
|
||||
}
|
||||
|
||||
fun logError(error: Throwable, message: String = error.message.orEmpty()) {
|
||||
TangemLogger.withTag(TX_HISTORY_TAG).e(message, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DefaultTxHistoryFetcherUtils @Inject constructor(
|
||||
appScope: AppCoroutineScope,
|
||||
override val analyticsEventHandler: AnalyticsEventHandler,
|
||||
override val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : TxHistoryFetcherUtils {
|
||||
|
||||
override val triggersBuffer: Channel<TxHistoryFetchTrigger> = Channel(Channel.BUFFERED)
|
||||
|
||||
// todo txhistory use lifecycle scope?
|
||||
override val fetcherScope: CoroutineScope = appScope + SupervisorJob()
|
||||
|
||||
override suspend fun sendTrigger(trigger: TxHistoryFetchTrigger) {
|
||||
triggersBuffer.trySend(trigger)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultTxHistoryRepository(
|
||||
class DefaultTxHistoryRepository @Inject constructor(
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
package com.tangem.data.txhistory.repository
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeItemResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPagination
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPaginationDelta
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse
|
||||
import com.tangem.datasource.local.converter.toEntity
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Fetches express (exchange & onramp) transaction history from the API and persists it into the local database.
|
||||
*
|
||||
*/
|
||||
internal class ExpressHistoryRepository @Inject constructor(
|
||||
private val exchangeApi: TangemExpressApi,
|
||||
private val onrampApi: OnrampApi,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
private val expressSyncStateDao: ExpressSyncStateDao,
|
||||
) {
|
||||
|
||||
suspend fun fetchExchangeHistory(
|
||||
fromAddress: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: Int = DEFAULT_LIMIT,
|
||||
): ExchangeHistoryResponse {
|
||||
val state = syncState(ExpressSyncStateEntity.Type.EXCHANGE, fromAddress)
|
||||
|
||||
val response = exchangeApi.getHistory(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromAddress = fromAddress,
|
||||
cursor = state?.afterCursor,
|
||||
limit = limit,
|
||||
).getOrThrow()
|
||||
|
||||
saveExchanges(ownerAddress = fromAddress, items = response.items)
|
||||
persistHistoryState(
|
||||
type = ExpressSyncStateEntity.Type.EXCHANGE,
|
||||
address = fromAddress,
|
||||
previous = state,
|
||||
pagination = response.pagination,
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun fetchExchangeHistoryDelta(
|
||||
fromAddress: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: Int = DEFAULT_LIMIT,
|
||||
): ExchangeHistoryDeltaResponse {
|
||||
val state = syncState(ExpressSyncStateEntity.Type.EXCHANGE, fromAddress)
|
||||
|
||||
val response = exchangeApi.getHistoryDelta(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromAddress = fromAddress,
|
||||
cursor = state?.deltaCursor,
|
||||
limit = limit,
|
||||
).getOrThrow()
|
||||
|
||||
saveExchanges(ownerAddress = fromAddress, items = response.items)
|
||||
persistDeltaState(
|
||||
type = ExpressSyncStateEntity.Type.EXCHANGE,
|
||||
address = fromAddress,
|
||||
pagination = response.pagination,
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun fetchOnrampHistory(
|
||||
payoutAddress: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: Int = DEFAULT_LIMIT,
|
||||
): OnrampHistoryResponse {
|
||||
val state = syncState(ExpressSyncStateEntity.Type.ONRAMP, payoutAddress)
|
||||
|
||||
val response = onrampApi.getHistory(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
payoutAddress = payoutAddress,
|
||||
afterCursor = state?.afterCursor,
|
||||
limit = limit,
|
||||
).getOrThrow()
|
||||
|
||||
saveOnramps(ownerAddress = payoutAddress, items = response.items)
|
||||
persistHistoryState(
|
||||
type = ExpressSyncStateEntity.Type.ONRAMP,
|
||||
address = payoutAddress,
|
||||
previous = state,
|
||||
pagination = response.pagination,
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun fetchOnrampHistoryDelta(
|
||||
payoutAddress: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: Int = DEFAULT_LIMIT,
|
||||
): OnrampHistoryDeltaResponse {
|
||||
val state = syncState(ExpressSyncStateEntity.Type.ONRAMP, payoutAddress)
|
||||
|
||||
val response = onrampApi.getHistoryDelta(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
payoutAddress = payoutAddress,
|
||||
cursor = state?.deltaCursor,
|
||||
limit = limit,
|
||||
).getOrThrow()
|
||||
|
||||
saveOnramps(ownerAddress = payoutAddress, items = response.items)
|
||||
persistDeltaState(
|
||||
type = ExpressSyncStateEntity.Type.ONRAMP,
|
||||
address = payoutAddress,
|
||||
pagination = response.pagination,
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun syncState(type: ExpressSyncStateEntity.Type, address: String): ExpressSyncStateEntity? {
|
||||
return expressSyncStateDao.observe(type = type.name, address = address).first()
|
||||
}
|
||||
|
||||
private suspend fun saveExchanges(ownerAddress: String, items: List<ExchangeItemResponse>) {
|
||||
expressHistoryDao.upsertExchanges(items.map { it.toEntity(ownerAddress) })
|
||||
}
|
||||
|
||||
private suspend fun saveOnramps(ownerAddress: String, items: List<OnrampItemResponse>) {
|
||||
expressHistoryDao.upsertOnramps(items.map { it.toEntity(ownerAddress) })
|
||||
}
|
||||
|
||||
private suspend fun persistHistoryState(
|
||||
type: ExpressSyncStateEntity.Type,
|
||||
address: String,
|
||||
previous: ExpressSyncStateEntity?,
|
||||
pagination: ExpressPagination,
|
||||
) {
|
||||
if (previous == null) {
|
||||
expressSyncStateDao.upsert(
|
||||
ExpressSyncStateEntity(
|
||||
type = type.name,
|
||||
address = address,
|
||||
isInitialCompleted = !pagination.hasMore,
|
||||
afterCursor = pagination.endCursor,
|
||||
deltaCursor = pagination.startDeltaCursor,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
expressSyncStateDao.updateHistoryCursor(
|
||||
type = type.name,
|
||||
address = address,
|
||||
afterCursor = pagination.endCursor,
|
||||
isInitialCompleted = !pagination.hasMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun persistDeltaState(
|
||||
type: ExpressSyncStateEntity.Type,
|
||||
address: String,
|
||||
pagination: ExpressPaginationDelta,
|
||||
) {
|
||||
val cursor = pagination.startCursor ?: return
|
||||
expressSyncStateDao.updateDeltaCursor(type = type.name, address = address, deltaCursor = cursor)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_LIMIT = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,9 @@ import com.tangem.pagination.BatchListSource
|
|||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RefactoredTxHistoryRepository(
|
||||
internal class RefactoredTxHistoryRepository @Inject constructor(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.test.mock.MockAccounts
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultAccountTxHistoryFetcherTest {
|
||||
|
||||
private val singleAccountSupplier: SingleAccountSupplier = mockk()
|
||||
private val paymentAccountCurrency: GetPaymentAccountCryptoCurrencyStatusUseCase = mockk()
|
||||
private val expressFetcherFactory: DefaultExpressTxHistoryFetcher.Factory = mockk()
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
|
||||
private val coin: CryptoCurrency = MockCryptoCurrencyFactory().ethereum
|
||||
|
||||
private val cryptoAccount = MockAccounts.createAccount(
|
||||
derivationIndex = 1,
|
||||
userWalletId = WALLET_ID,
|
||||
cryptoCurrencies = listOf(coin),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(singleAccountSupplier, paymentAccountCurrency, expressFetcherFactory, walletManagersFacade)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates express fetcher per coin address of a crypto portfolio account`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountFlow = MutableStateFlow<Account>(cryptoAccount)
|
||||
every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow
|
||||
coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS
|
||||
val expressFetcher = relaxedExpressFetcher()
|
||||
every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher
|
||||
|
||||
// Act
|
||||
val fetcher = createFetcher(cryptoAccount.accountId, utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS)
|
||||
verify(exactly = 1) { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closes express fetcher when its coin is removed from the account`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountFlow = MutableStateFlow<Account>(cryptoAccount)
|
||||
every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow
|
||||
coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS
|
||||
val expressFetcher = relaxedExpressFetcher()
|
||||
every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher
|
||||
|
||||
val fetcher = createFetcher(cryptoAccount.accountId, utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS)
|
||||
|
||||
// Act
|
||||
accountFlow.value = cryptoAccount.copy(cryptoCurrencies = emptyList())
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.expressFetchers).isEmpty()
|
||||
verify(exactly = 1) { expressFetcher.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `routes trigger to the express fetcher of the currency address`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountFlow = MutableStateFlow<Account>(cryptoAccount)
|
||||
every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow
|
||||
coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS
|
||||
val expressFetcher = relaxedExpressFetcher()
|
||||
every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher
|
||||
|
||||
val fetcher = createFetcher(cryptoAccount.accountId, utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = coin)
|
||||
fetcher.invoke(trigger)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { expressFetcher.invoke(trigger) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close cancels scope and closes all express fetchers`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountFlow = MutableStateFlow<Account>(cryptoAccount)
|
||||
every { singleAccountSupplier.invoke(cryptoAccount.accountId) } returns accountFlow
|
||||
coEvery { walletManagersFacade.getDefaultAddress(WALLET_ID, coin.network) } returns ADDRESS
|
||||
val expressFetcher = relaxedExpressFetcher()
|
||||
every { expressFetcherFactory.create(ADDRESS, cryptoAccount.accountId) } returns expressFetcher
|
||||
|
||||
val fetcher = createFetcher(cryptoAccount.accountId, utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.expressFetchers.keys).containsExactly(ADDRESS)
|
||||
|
||||
// Act
|
||||
fetcher.close()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.expressFetchers).isEmpty()
|
||||
verify(exactly = 1) { expressFetcher.close() }
|
||||
assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse()
|
||||
}
|
||||
|
||||
private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils(
|
||||
appScope = TestAppCoroutineScope(testScope = this),
|
||||
analyticsEventHandler = mockk(relaxed = true),
|
||||
analyticsExceptionHandler = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
private fun createFetcher(accountId: AccountId, utils: DefaultTxHistoryFetcherUtils) =
|
||||
DefaultAccountTxHistoryFetcher(
|
||||
accountId = accountId,
|
||||
utils = utils,
|
||||
singleAccountSupplier = singleAccountSupplier,
|
||||
paymentAccountCurrency = paymentAccountCurrency,
|
||||
expressFetcherFactory = expressFetcherFactory,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
|
||||
private fun relaxedExpressFetcher() = mockk<DefaultExpressTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID = MockAccounts.userWalletId
|
||||
const val ADDRESS = "0xEthAddress"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultAppTxHistoryFetcherTest {
|
||||
|
||||
private val getWalletsUseCase: GetWalletsUseCase = mockk()
|
||||
private val selectedWalletUseCase: GetSelectedWalletUseCase = mockk()
|
||||
private val walletFetcherFactory: DefaultWalletTxHistoryFetcher.Factory = mockk()
|
||||
|
||||
private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(getWalletsUseCase, selectedWalletUseCase, walletFetcherFactory)
|
||||
every { selectedWalletUseCase.selectedFlow() } returns emptyFlow()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates wallet fetcher for each new wallet`() = runTest {
|
||||
val utils = createUtils()
|
||||
val walletsFlow = MutableStateFlow(linkedMapOf<UserWalletId, UserWallet>())
|
||||
every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow
|
||||
val walletFetcher1 = relaxedWalletFetcher()
|
||||
every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.fetchers).isEmpty()
|
||||
|
||||
// Act
|
||||
walletsFlow.value = linkedMapOf(WALLET_ID_1 to mockk())
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1)
|
||||
verify(exactly = 1) { walletFetcherFactory.create(WALLET_ID_1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closes and removes fetcher when wallet is removed`() = runTest {
|
||||
val utils = createUtils()
|
||||
val walletsFlow = MutableStateFlow(
|
||||
linkedMapOf<UserWalletId, UserWallet>(WALLET_ID_1 to mockk(), WALLET_ID_2 to mockk()),
|
||||
)
|
||||
every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow
|
||||
val walletFetcher1 = relaxedWalletFetcher()
|
||||
val walletFetcher2 = relaxedWalletFetcher()
|
||||
every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1
|
||||
every { walletFetcherFactory.create(WALLET_ID_2) } returns walletFetcher2
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1, WALLET_ID_2)
|
||||
|
||||
// Act
|
||||
walletsFlow.value = linkedMapOf(WALLET_ID_1 to mockk())
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1)
|
||||
verify(exactly = 1) { walletFetcher2.close() }
|
||||
verify(inverse = true) { walletFetcher1.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `routes trigger to the fetcher of the target wallet`() = runTest {
|
||||
val utils = createUtils()
|
||||
val walletsFlow = MutableStateFlow(linkedMapOf<UserWalletId, UserWallet>(WALLET_ID_1 to mockk()))
|
||||
every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow
|
||||
val walletFetcher1 = relaxedWalletFetcher()
|
||||
every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID_1, currency = currency)
|
||||
fetcher.invoke(trigger)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { walletFetcher1.invoke(trigger) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does nothing when trigger targets unknown wallet`() = runTest {
|
||||
val utils = createUtils()
|
||||
val walletsFlow = MutableStateFlow(linkedMapOf<UserWalletId, UserWallet>())
|
||||
every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID_1, currency = currency)
|
||||
val result = fetcher.invoke(trigger)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers).isEmpty()
|
||||
verify(inverse = true) { walletFetcherFactory.create(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close cancels scope and closes all child fetchers`() = runTest {
|
||||
val utils = createUtils()
|
||||
val walletsFlow = MutableStateFlow(linkedMapOf<UserWalletId, UserWallet>(WALLET_ID_1 to mockk()))
|
||||
every { getWalletsUseCase.invokeAsMap(any(), any()) } returns walletsFlow
|
||||
val walletFetcher1 = relaxedWalletFetcher()
|
||||
every { walletFetcherFactory.create(WALLET_ID_1) } returns walletFetcher1
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(WALLET_ID_1)
|
||||
|
||||
// Act
|
||||
fetcher.close()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers).isEmpty()
|
||||
verify(exactly = 1) { walletFetcher1.close() }
|
||||
assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse()
|
||||
}
|
||||
|
||||
private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils(
|
||||
appScope = TestAppCoroutineScope(testScope = this),
|
||||
analyticsEventHandler = mockk(relaxed = true),
|
||||
analyticsExceptionHandler = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
private fun createFetcher(utils: DefaultTxHistoryFetcherUtils) = DefaultAppTxHistoryFetcher(
|
||||
utils = utils,
|
||||
getWalletsUseCase = getWalletsUseCase,
|
||||
selectedWalletUseCase = selectedWalletUseCase,
|
||||
walletTxHistoryFetcherFactory = walletFetcherFactory,
|
||||
)
|
||||
|
||||
private fun relaxedWalletFetcher() = mockk<DefaultWalletTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID_1 = UserWalletId("001")
|
||||
val WALLET_ID_2 = UserWalletId("002")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.txhistory.repository.ExpressHistoryRepository
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPagination
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPaginationDelta
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.test.mock.MockAccounts
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultExpressTxHistoryFetcherTest {
|
||||
|
||||
private val expressSyncStateDao: ExpressSyncStateDao = mockk()
|
||||
private val expressHistoryRepository: ExpressHistoryRepository = mockk()
|
||||
|
||||
private val coin: CryptoCurrency = MockCryptoCurrencyFactory().ethereum
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(expressSyncStateDao, expressHistoryRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exposes the address it was created with`() = runTest {
|
||||
val fetcher = createFetcher(createUtils())
|
||||
|
||||
assertThat(fetcher.address).isEqualTo(ADDRESS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on first trigger fetches initial exchange and onramp history for the address`() = runTest {
|
||||
stubAllSuccess(hasMore = false)
|
||||
val fetcher = createFetcher(createUtils())
|
||||
|
||||
// Act
|
||||
fetcher.invoke(TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = coin))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(atLeast = 1) { expressHistoryRepository.fetchExchangeHistory(ADDRESS, any()) }
|
||||
coVerify(atLeast = 1) { expressHistoryRepository.fetchOnrampHistory(ADDRESS, any()) }
|
||||
coVerify(exactly = 1) { expressHistoryRepository.fetchExchangeHistoryDelta(ADDRESS, any()) }
|
||||
coVerify(exactly = 1) { expressHistoryRepository.fetchOnrampHistoryDelta(ADDRESS, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `continues exchange initial pagination while hasMore is true`() = runTest {
|
||||
every { expressSyncStateDao.observe(any(), ADDRESS) } returns flowOf(null)
|
||||
// 1st call: initial fetch in fetchExchange (pagination ignored)
|
||||
// 2nd call: pagination loop, hasMore = true -> continue
|
||||
// 3rd call: pagination loop, hasMore = false -> stop
|
||||
coEvery { expressHistoryRepository.fetchExchangeHistory(ADDRESS, any()) } returnsMany listOf(
|
||||
exchangeResponse(hasMore = true),
|
||||
exchangeResponse(hasMore = true),
|
||||
exchangeResponse(hasMore = false),
|
||||
)
|
||||
coEvery { expressHistoryRepository.fetchExchangeHistoryDelta(ADDRESS, any()) } returns
|
||||
exchangeDeltaResponse(hasMore = false)
|
||||
coEvery { expressHistoryRepository.fetchOnrampHistory(ADDRESS, any()) } returns onrampResponse(hasMore = false)
|
||||
coEvery { expressHistoryRepository.fetchOnrampHistoryDelta(ADDRESS, any()) } returns
|
||||
onrampDeltaResponse(hasMore = false)
|
||||
val fetcher = createFetcher(createUtils())
|
||||
|
||||
// Act
|
||||
fetcher.invoke(TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = coin))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 3) { expressHistoryRepository.fetchExchangeHistory(ADDRESS, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `skips initial pagination when it is already completed`() = runTest {
|
||||
every { expressSyncStateDao.observe(any(), ADDRESS) } returns flowOf(completedSyncState())
|
||||
coEvery { expressHistoryRepository.fetchExchangeHistoryDelta(ADDRESS, any()) } returns
|
||||
exchangeDeltaResponse(hasMore = false)
|
||||
coEvery { expressHistoryRepository.fetchOnrampHistoryDelta(ADDRESS, any()) } returns
|
||||
onrampDeltaResponse(hasMore = false)
|
||||
val fetcher = createFetcher(createUtils())
|
||||
|
||||
// Act
|
||||
fetcher.invoke(TxHistoryFetchTrigger.TokenDetailsPTR(walletId = WALLET_ID, currency = coin))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert: no initial history fetch, only the delta pagination runs
|
||||
coVerify(exactly = 0) { expressHistoryRepository.fetchExchangeHistory(any(), any()) }
|
||||
coVerify(exactly = 0) { expressHistoryRepository.fetchOnrampHistory(any(), any()) }
|
||||
coVerify(exactly = 1) { expressHistoryRepository.fetchExchangeHistoryDelta(ADDRESS, any()) }
|
||||
coVerify(exactly = 1) { expressHistoryRepository.fetchOnrampHistoryDelta(ADDRESS, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close cancels the fetcher scope`() = runTest {
|
||||
val utils = createUtils()
|
||||
val fetcher = createFetcher(utils)
|
||||
|
||||
// Act
|
||||
fetcher.close()
|
||||
|
||||
// Assert
|
||||
assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse()
|
||||
}
|
||||
|
||||
private fun stubAllSuccess(hasMore: Boolean) {
|
||||
every { expressSyncStateDao.observe(any(), ADDRESS) } returns flowOf(null)
|
||||
coEvery { expressHistoryRepository.fetchExchangeHistory(ADDRESS, any()) } returns exchangeResponse(hasMore)
|
||||
coEvery { expressHistoryRepository.fetchExchangeHistoryDelta(ADDRESS, any()) } returns
|
||||
exchangeDeltaResponse(hasMore)
|
||||
coEvery { expressHistoryRepository.fetchOnrampHistory(ADDRESS, any()) } returns onrampResponse(hasMore)
|
||||
coEvery { expressHistoryRepository.fetchOnrampHistoryDelta(ADDRESS, any()) } returns onrampDeltaResponse(hasMore)
|
||||
}
|
||||
|
||||
private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils(
|
||||
appScope = TestAppCoroutineScope(testScope = this),
|
||||
analyticsEventHandler = mockk(relaxed = true),
|
||||
analyticsExceptionHandler = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
private fun createFetcher(utils: DefaultTxHistoryFetcherUtils) = DefaultExpressTxHistoryFetcher(
|
||||
address = ADDRESS,
|
||||
accountId = ACCOUNT_ID,
|
||||
utils = utils,
|
||||
expressSyncStateDao = expressSyncStateDao,
|
||||
expressHistoryRepository = expressHistoryRepository,
|
||||
)
|
||||
|
||||
private fun exchangeResponse(hasMore: Boolean) =
|
||||
ExchangeHistoryResponse(items = emptyList(), pagination = pagination(hasMore))
|
||||
|
||||
private fun exchangeDeltaResponse(hasMore: Boolean) =
|
||||
ExchangeHistoryDeltaResponse(items = emptyList(), pagination = paginationDelta(hasMore))
|
||||
|
||||
private fun onrampResponse(hasMore: Boolean) =
|
||||
OnrampHistoryResponse(items = emptyList(), pagination = pagination(hasMore))
|
||||
|
||||
private fun onrampDeltaResponse(hasMore: Boolean) =
|
||||
OnrampHistoryDeltaResponse(items = emptyList(), pagination = paginationDelta(hasMore))
|
||||
|
||||
private fun pagination(hasMore: Boolean) =
|
||||
ExpressPagination(endCursor = null, startDeltaCursor = null, hasMore = hasMore)
|
||||
|
||||
private fun paginationDelta(hasMore: Boolean) = ExpressPaginationDelta(startCursor = null, hasMore = hasMore)
|
||||
|
||||
private fun completedSyncState() = ExpressSyncStateEntity(
|
||||
type = ExpressSyncStateEntity.Type.EXCHANGE.name,
|
||||
address = ADDRESS,
|
||||
isInitialCompleted = true,
|
||||
afterCursor = null,
|
||||
deltaCursor = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID = MockAccounts.userWalletId
|
||||
val ACCOUNT_ID = AccountId.forMainCryptoPortfolio(WALLET_ID)
|
||||
const val ADDRESS = "0xEthAddress"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.tangem.data.txhistory.fetcher
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.txhistory.fetcher.TxHistoryFetchTrigger
|
||||
import com.tangem.test.mock.MockAccounts
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultWalletTxHistoryFetcherTest {
|
||||
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
|
||||
private val accountFetcherFactory: DefaultAccountTxHistoryFetcher.Factory = mockk()
|
||||
|
||||
private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum
|
||||
|
||||
private val mainAccount = Account.CryptoPortfolio.createMainAccount(
|
||||
userWalletId = WALLET_ID,
|
||||
cryptoCurrencies = listOf(currency),
|
||||
)
|
||||
private val secondAccount = MockAccounts.createAccount(derivationIndex = 1, userWalletId = WALLET_ID)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(singleAccountListSupplier, accountFetcherFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates account fetcher for each account in the wallet`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount))
|
||||
every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow
|
||||
val mainFetcher = relaxedAccountFetcher()
|
||||
val secondFetcher = relaxedAccountFetcher()
|
||||
every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher
|
||||
every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher
|
||||
|
||||
// Act
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId)
|
||||
verify(exactly = 1) { accountFetcherFactory.create(mainAccount.accountId) }
|
||||
verify(exactly = 1) { accountFetcherFactory.create(secondAccount.accountId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closes and removes fetcher when account is removed`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount))
|
||||
every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow
|
||||
val mainFetcher = relaxedAccountFetcher()
|
||||
val secondFetcher = relaxedAccountFetcher()
|
||||
every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher
|
||||
every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId)
|
||||
|
||||
// Act
|
||||
accountListFlow.value = accountListOf(mainAccount)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId)
|
||||
verify(exactly = 1) { secondFetcher.close() }
|
||||
verify(inverse = true) { mainFetcher.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `routes trigger to the fetcher of the account that holds the currency`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountListFlow = MutableStateFlow(accountListOf(mainAccount))
|
||||
every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow
|
||||
val mainFetcher = relaxedAccountFetcher()
|
||||
every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsPTR(walletId = WALLET_ID, currency = currency)
|
||||
fetcher.invoke(trigger)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { mainFetcher.invoke(trigger) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does nothing when trigger currency is not present in any account`() = runTest {
|
||||
val utils = createUtils()
|
||||
// main account without the triggered currency
|
||||
val accountListFlow = MutableStateFlow(accountListOf(Account.CryptoPortfolio.createMainAccount(WALLET_ID)))
|
||||
every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow
|
||||
val mainFetcher = relaxedAccountFetcher()
|
||||
every { accountFetcherFactory.create(any()) } returns mainFetcher
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
val trigger = TxHistoryFetchTrigger.TokenDetailsOpen(walletId = WALLET_ID, currency = currency)
|
||||
val result = fetcher.invoke(trigger)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(inverse = true) { mainFetcher.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close cancels scope and closes all child fetchers`() = runTest {
|
||||
val utils = createUtils()
|
||||
val accountListFlow = MutableStateFlow(accountListOf(mainAccount, secondAccount))
|
||||
every { singleAccountListSupplier.invoke(WALLET_ID) } returns accountListFlow
|
||||
val mainFetcher = relaxedAccountFetcher()
|
||||
val secondFetcher = relaxedAccountFetcher()
|
||||
every { accountFetcherFactory.create(mainAccount.accountId) } returns mainFetcher
|
||||
every { accountFetcherFactory.create(secondAccount.accountId) } returns secondFetcher
|
||||
|
||||
val fetcher = createFetcher(utils)
|
||||
advanceUntilIdle()
|
||||
assertThat(fetcher.fetchers.keys).containsExactly(mainAccount.accountId, secondAccount.accountId)
|
||||
|
||||
// Act
|
||||
fetcher.close()
|
||||
|
||||
// Assert
|
||||
assertThat(fetcher.fetchers).isEmpty()
|
||||
verify(exactly = 1) { mainFetcher.close() }
|
||||
verify(exactly = 1) { secondFetcher.close() }
|
||||
assertThat(utils.fetcherScope.coroutineContext.job.isActive).isFalse()
|
||||
}
|
||||
|
||||
private fun accountListOf(vararg accounts: Account): AccountList = AccountList(
|
||||
userWalletId = WALLET_ID,
|
||||
accounts = accounts.toList(),
|
||||
totalAccounts = accounts.size,
|
||||
totalArchivedAccounts = 0,
|
||||
).getOrNull()!!
|
||||
|
||||
private fun TestScope.createUtils(): DefaultTxHistoryFetcherUtils = DefaultTxHistoryFetcherUtils(
|
||||
appScope = TestAppCoroutineScope(testScope = this),
|
||||
analyticsEventHandler = mockk(relaxed = true),
|
||||
analyticsExceptionHandler = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
private fun createFetcher(utils: DefaultTxHistoryFetcherUtils) = DefaultWalletTxHistoryFetcher(
|
||||
walletId = WALLET_ID,
|
||||
utils = utils,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
accountTxHistoryFetcher = accountFetcherFactory,
|
||||
)
|
||||
|
||||
private fun relaxedAccountFetcher() = mockk<DefaultAccountTxHistoryFetcher>(relaxed = true)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID: UserWalletId = MockAccounts.userWalletId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
package com.tangem.data.txhistory.repository
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.converter.toEntity
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeHistoryResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeItemResponse
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPagination
|
||||
import com.tangem.datasource.api.express.models.response.ExpressPaginationDelta
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse
|
||||
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ExpressHistoryRepositoryTest {
|
||||
|
||||
private val exchangeApi: TangemExpressApi = mockk()
|
||||
private val onrampApi: OnrampApi = mockk()
|
||||
private val expressHistoryDao: ExpressHistoryDao = mockk(relaxUnitFun = true)
|
||||
private val expressSyncStateDao: ExpressSyncStateDao = mockk(relaxUnitFun = true)
|
||||
|
||||
private val repository = ExpressHistoryRepository(
|
||||
exchangeApi = exchangeApi,
|
||||
onrampApi = onrampApi,
|
||||
expressHistoryDao = expressHistoryDao,
|
||||
expressSyncStateDao = expressSyncStateDao,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(exchangeApi, onrampApi, expressHistoryDao, expressSyncStateDao)
|
||||
}
|
||||
|
||||
// region exchange history
|
||||
|
||||
@Test
|
||||
fun `GIVEN sync state WHEN fetchExchangeHistory THEN passes after cursor and persists items`() = runTest {
|
||||
// GIVEN
|
||||
val item = createExchangeItem()
|
||||
val response = ExchangeHistoryResponse(items = listOf(item), pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
coEvery {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
val result = repository.fetchExchangeHistory(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
coVerify(exactly = 1) { expressHistoryDao.upsertExchanges(listOf(item.toEntity(ADDRESS))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no sync state WHEN fetchExchangeHistory THEN passes null cursor`() = runTest {
|
||||
// GIVEN
|
||||
val response = ExchangeHistoryResponse(items = emptyList(), pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, state = null)
|
||||
coEvery {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = null, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
repository.fetchExchangeHistory(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = null, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom limit WHEN fetchExchangeHistory THEN forwards limit to api`() = runTest {
|
||||
// GIVEN
|
||||
val response = ExchangeHistoryResponse(items = emptyList(), pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
coEvery {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
repository.fetchExchangeHistory(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID, limit = 25)
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = 25)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN api error WHEN fetchExchangeHistory THEN throws and does not persist`() = runTest {
|
||||
// GIVEN
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
val error = httpError()
|
||||
coEvery {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Error(error).cast()
|
||||
|
||||
// WHEN
|
||||
val thrown = runCatching { repository.fetchExchangeHistory(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID) }.exceptionOrNull()
|
||||
|
||||
// THEN
|
||||
assertThat(thrown).isEqualTo(error)
|
||||
coVerify(exactly = 0) { expressHistoryDao.upsertExchanges(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sync state WHEN fetchExchangeHistoryDelta THEN passes delta cursor and persists items`() = runTest {
|
||||
// GIVEN
|
||||
val item = createExchangeItem()
|
||||
val response = ExchangeHistoryDeltaResponse(items = listOf(item), pagination = paginationDelta())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, syncState(deltaCursor = DELTA_CURSOR))
|
||||
coEvery {
|
||||
exchangeApi.getHistoryDelta(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = DELTA_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
val result = repository.fetchExchangeHistoryDelta(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) {
|
||||
exchangeApi.getHistoryDelta(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = DELTA_CURSOR, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
coVerify(exactly = 1) { expressHistoryDao.upsertExchanges(listOf(item.toEntity(ADDRESS))) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region onramp history
|
||||
|
||||
@Test
|
||||
fun `GIVEN sync state WHEN fetchOnrampHistory THEN passes after cursor and persists items`() = runTest {
|
||||
// GIVEN
|
||||
val item = createOnrampItem()
|
||||
val response = OnrampHistoryResponse(items = listOf(item), pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.ONRAMP, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
coEvery {
|
||||
onrampApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, afterCursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
val result = repository.fetchOnrampHistory(payoutAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) {
|
||||
onrampApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, afterCursor = AFTER_CURSOR, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
coVerify(exactly = 1) { expressHistoryDao.upsertOnramps(listOf(item.toEntity(ADDRESS))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no sync state WHEN fetchOnrampHistory THEN passes null cursor`() = runTest {
|
||||
// GIVEN
|
||||
val response = OnrampHistoryResponse(items = emptyList(), pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.ONRAMP, ADDRESS, state = null)
|
||||
coEvery {
|
||||
onrampApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, afterCursor = null, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
repository.fetchOnrampHistory(payoutAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) {
|
||||
onrampApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, afterCursor = null, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sync state WHEN fetchOnrampHistoryDelta THEN passes delta cursor and persists items`() = runTest {
|
||||
// GIVEN
|
||||
val item = createOnrampItem()
|
||||
val response = OnrampHistoryDeltaResponse(items = listOf(item), pagination = paginationDelta())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.ONRAMP, ADDRESS, syncState(deltaCursor = DELTA_CURSOR))
|
||||
coEvery {
|
||||
onrampApi.getHistoryDelta(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, cursor = DELTA_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
|
||||
// WHEN
|
||||
val result = repository.fetchOnrampHistoryDelta(payoutAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) {
|
||||
onrampApi.getHistoryDelta(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, cursor = DELTA_CURSOR, limit = DEFAULT_LIMIT)
|
||||
}
|
||||
coVerify(exactly = 1) { expressHistoryDao.upsertOnramps(listOf(item.toEntity(ADDRESS))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN api error WHEN fetchOnrampHistory THEN throws and does not persist`() = runTest {
|
||||
// GIVEN
|
||||
stubSyncState(ExpressSyncStateEntity.Type.ONRAMP, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
val error = httpError()
|
||||
coEvery {
|
||||
onrampApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, payoutAddress = ADDRESS, afterCursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Error(error).cast()
|
||||
|
||||
// WHEN
|
||||
val thrown = runCatching { repository.fetchOnrampHistory(payoutAddress = ADDRESS, userWalletId = USER_WALLET_ID) }.exceptionOrNull()
|
||||
|
||||
// THEN
|
||||
assertThat(thrown).isEqualTo(error)
|
||||
coVerify(exactly = 0) { expressHistoryDao.upsertOnramps(any()) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region syncState
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored sync state WHEN syncState THEN returns first emitted value`() = runTest {
|
||||
// GIVEN
|
||||
val state = syncState(afterCursor = AFTER_CURSOR, deltaCursor = DELTA_CURSOR)
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, state)
|
||||
|
||||
// WHEN
|
||||
val result = repository.syncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple items WHEN fetchExchangeHistory THEN maps every item with owner address`() = runTest {
|
||||
// GIVEN
|
||||
val items = listOf(
|
||||
createExchangeItem(txId = "tx-1"),
|
||||
createExchangeItem(txId = "tx-2"),
|
||||
)
|
||||
val response = ExchangeHistoryResponse(items = items, pagination = pagination())
|
||||
stubSyncState(ExpressSyncStateEntity.Type.EXCHANGE, ADDRESS, syncState(afterCursor = AFTER_CURSOR))
|
||||
coEvery {
|
||||
exchangeApi.getHistory(userWalletId = USER_WALLET_ID_VALUE, fromAddress = ADDRESS, cursor = AFTER_CURSOR, limit = any())
|
||||
} returns ApiResponse.Success(response)
|
||||
val saved = slot<List<ExpressExchangeEntity>>()
|
||||
coEvery { expressHistoryDao.upsertExchanges(capture(saved)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
repository.fetchExchangeHistory(fromAddress = ADDRESS, userWalletId = USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(saved.captured).isEqualTo(items.map { it.toEntity(ADDRESS) })
|
||||
assertThat(saved.captured.map { it.ownerAddress }.toSet()).containsExactly(ADDRESS)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun stubSyncState(type: ExpressSyncStateEntity.Type, address: String, state: ExpressSyncStateEntity?) {
|
||||
coEvery { expressSyncStateDao.observe(type = type.name, address = address) } returns flowOf(state)
|
||||
}
|
||||
|
||||
private fun syncState(afterCursor: String? = null, deltaCursor: String? = null) = ExpressSyncStateEntity(
|
||||
type = ExpressSyncStateEntity.Type.EXCHANGE.name,
|
||||
address = ADDRESS,
|
||||
isInitialCompleted = true,
|
||||
afterCursor = afterCursor,
|
||||
deltaCursor = deltaCursor,
|
||||
)
|
||||
|
||||
private fun pagination() = ExpressPagination(endCursor = "end", startDeltaCursor = "delta", hasMore = false)
|
||||
|
||||
private fun paginationDelta() = ExpressPaginationDelta(startCursor = "start", hasMore = false)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Any> ApiResponse.Error.cast(): ApiResponse<T> = this as ApiResponse<T>
|
||||
|
||||
private fun httpError() = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.INTERNAL_SERVER_ERROR,
|
||||
message = "boom",
|
||||
errorBody = null,
|
||||
)
|
||||
|
||||
private fun createExchangeItem(txId: String = "exchange-tx-1") = ExchangeItemResponse(
|
||||
txId = txId,
|
||||
providerId = "changelly",
|
||||
fromAddress = "0xfrom",
|
||||
payinAddress = "0xpayin",
|
||||
payinExtraId = null,
|
||||
payoutAddress = "0xpayout",
|
||||
refundAddress = null,
|
||||
refundExtraId = null,
|
||||
rateType = "float",
|
||||
status = "finished",
|
||||
externalTxId = null,
|
||||
externalTxUrl = null,
|
||||
payinHash = "payin-hash",
|
||||
payoutHash = "payout-hash",
|
||||
refundNetwork = null,
|
||||
refundContractAddress = null,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
payTill = null,
|
||||
averageDuration = null,
|
||||
fromContractAddress = "0xfromContract",
|
||||
fromNetwork = "ethereum",
|
||||
fromDecimals = 18,
|
||||
fromAmount = "1.0",
|
||||
toContractAddress = "0xtoContract",
|
||||
toNetwork = "bitcoin",
|
||||
toDecimals = 8,
|
||||
toAmount = "1.0",
|
||||
toActualAmount = "0.99",
|
||||
)
|
||||
|
||||
private fun createOnrampItem(txId: String = "onramp-tx-1") = OnrampItemResponse(
|
||||
txId = txId,
|
||||
providerId = "mercuryo",
|
||||
payoutAddress = "0xpayout",
|
||||
status = "finished",
|
||||
failReason = null,
|
||||
externalTxId = null,
|
||||
externalTxUrl = null,
|
||||
payoutHash = "payout-hash",
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
fromCurrencyCode = "USD",
|
||||
fromAmount = "100.0",
|
||||
fromPrecision = 2,
|
||||
toContractAddress = "0xtoContract",
|
||||
toNetwork = "bitcoin",
|
||||
toDecimals = 8,
|
||||
toAmount = "0.001",
|
||||
toActualAmount = "0.99",
|
||||
paymentMethod = "card",
|
||||
countryCode = "US",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val ADDRESS = "0xowner"
|
||||
val USER_WALLET_ID = UserWalletId("0123456789abcdef")
|
||||
val USER_WALLET_ID_VALUE = USER_WALLET_ID.stringValue
|
||||
const val AFTER_CURSOR = "after-cursor"
|
||||
const val DELTA_CURSOR = "delta-cursor"
|
||||
const val DEFAULT_LIMIT = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -21,11 +21,6 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -84,7 +79,5 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
<ID>MaxChainedCallsOnSameLine:DefaultVisaRepository.kt$DefaultVisaRepository$userWallet.requireColdWallet().scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultVisaActivationRepository.kt$DefaultVisaActivationRepository${ VisaDataToSignByCardWallet( request = request, hashToSign = it.result.hash, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultVisaActivationRepository.kt$DefaultVisaActivationRepository${ VisaDataToSignByCustomerWallet( request = request, hashToSign = it.result.hash, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:VisaApiRequestMaker.kt$VisaApiRequestMaker${ if (it is ApiResponseError.HttpException && it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED ) { userWalletsStore.update(userWalletId) { userWallet -> userWallet.requireColdWallet().copy( scanResponse = userWallet.scanResponse.copy( // visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired, ), ) } } throw RefreshTokenExpiredException() }</ID>
|
||||
<ID>MultilineLambdaItParameter:VisaTxDetailsFactory.kt$VisaTxDetailsFactory${ when (val txUrl = walletBlockchain.getExploreTxUrl(it)) { is TxExploreState.Url -> txUrl.url is TxExploreState.Unsupported -> "" } }</ID>
|
||||
<ID>MultilineLambdaItParameter:VisaTxHistoryPagingSource.kt$VisaTxHistoryPagingSource${ it.toMutableMap().apply { this[cardPublicKey] = this[cardPublicKey].orEmpty() + response.transactions } }</ID>
|
||||
<ID>MultilineLambdaItParameter:VisaTxHistoryPagingSource.kt$VisaTxHistoryPagingSource${ it.toMutableMap().apply { this[offset] = response.transactions.map(VisaTxHistoryItemConverter::convert) } }</ID>
|
||||
|
|
@ -13,8 +12,6 @@
|
|||
<ID>NoNameShadowing:DefaultVisaActivationRepository.kt$DefaultVisaActivationRepository$responseError</ID>
|
||||
<ID>NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (config != null) return@withLock requireNotNull(config)</ID>
|
||||
<ID>NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (provider != null) return@withLock requireNotNull(provider)</ID>
|
||||
<ID>NullableToStringCall:DefaultOnboardingRepository.kt$DefaultOnboardingRepository$${error.message}</ID>
|
||||
<ID>RedundantSuspendModifier:DefaultVisaRepository.kt$DefaultVisaRepository$suspend</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:DefaultVisaRepository.kt$DefaultVisaRepository$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:VisaApiRequestMaker.kt$VisaApiRequestMaker$runCatching</ID>
|
||||
<ID>UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$if (status is VisaCardActivationStatus.RefreshTokenExpired) { throw RefreshTokenExpiredException() }</ID>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.models.pay.*
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import javax.inject.Inject
|
||||
|
|
@ -37,22 +34,24 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
|
||||
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
|
||||
customerId = value.customerId,
|
||||
currencyCode = value.currencyCode,
|
||||
currencyCode = value.balance.fiatBalance.currency,
|
||||
depositAddress = value.depositAddress,
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
fiatBalance = value.balance.fiatBalance.toDM(),
|
||||
cryptoBalance = value.balance.cryptoBalance.toDM(),
|
||||
availableForWithdrawal = value.balance.availableForWithdrawal,
|
||||
fiatRate = value.fiatRate,
|
||||
cards = value.cards.map { card ->
|
||||
PaymentAccountStatusValueDM.TangemPayCard(
|
||||
id = card.id,
|
||||
productInstanceId = card.productInstanceId,
|
||||
cardStatus = card.cardStatus.name,
|
||||
hasPinCode = card.hasPinCode,
|
||||
displayName = card.displayName?.value,
|
||||
actualDailyLimit = card.limit?.actualCardLimit?.amount,
|
||||
adminDailyLimit = card.limit?.adminCardLimit?.amount,
|
||||
isFrozen = card.isFrozen,
|
||||
frozenState = card.frozenState.toString(),
|
||||
lastDigits = card.lastDigits,
|
||||
isReissuing = card.isReissuing,
|
||||
state = card.state.toString(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -61,9 +60,11 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
|
||||
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
|
||||
customerId = value.customerId,
|
||||
fiatRate = value.fiatRate,
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
fiatBalance = value.balance.fiatBalance.toDM(),
|
||||
cryptoBalance = value.balance.cryptoBalance.toDM(),
|
||||
availableForWithdrawal = value.balance.availableForWithdrawal,
|
||||
)
|
||||
// Transient statuses are not persisted
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
|
|
@ -88,16 +89,19 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.CACHE,
|
||||
customerId = value.customerId,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = value.fiatRate,
|
||||
cards = value.cards.map { card ->
|
||||
TangemPayCard(
|
||||
id = card.id,
|
||||
productInstanceId = card.productInstanceId,
|
||||
cardStatus = TangemPayCard.Status.fromString(card.cardStatus),
|
||||
hasPinCode = card.hasPinCode,
|
||||
displayName = card.displayName?.let { CardDisplayName(it).getOrElse { null } },
|
||||
limit = TangemPayCardLimitData(
|
||||
|
|
@ -108,11 +112,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
|
||||
},
|
||||
),
|
||||
isFrozen = card.isFrozen,
|
||||
frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
|
||||
lastDigits = card.lastDigits,
|
||||
isReissuing = card.isReissuing,
|
||||
state = TangemPayCardState.fromString(card.state),
|
||||
)
|
||||
},
|
||||
error = null,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
|
|
@ -121,10 +126,15 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.CACHE,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
customerId = value.customerId,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = value.fiatRate,
|
||||
error = null,
|
||||
)
|
||||
null -> PaymentAccountStatusValue.Error.Unavailable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
|||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
|
|
@ -30,6 +31,7 @@ import com.tangem.domain.pay.usecase.*
|
|||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -61,10 +63,18 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCustomerOffersRepository(repository: DefaultCustomerOffersRepository): CustomerOffersRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory
|
||||
|
|
@ -75,6 +85,12 @@ internal interface TangemPayDataModule {
|
|||
impl: DefaultGetTangemPayCurrencyStatusUseCase,
|
||||
): GetTangemPayCurrencyStatusUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayWithdrawWithSwapUseCase(
|
||||
impl: DefaultTangemPayWithdrawWithSwapUseCase,
|
||||
): TangemPayWithdrawWithSwapUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
|
@ -204,5 +220,57 @@ internal interface TangemPayDataModule {
|
|||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCloseTangemPayCardUseCase(
|
||||
closeCardRepository: TangemPayCloseCardRepository,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CloseTangemPayCardUseCase {
|
||||
return CloseTangemPayCardUseCase(
|
||||
closeCardRepository = closeCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetCustomerOffersUseCase(
|
||||
customerOffersRepository: CustomerOffersRepository,
|
||||
): GetCustomerOffersUseCase {
|
||||
return GetCustomerOffersUseCase(customerOffersRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCheckOrderConflictUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
): CheckOrderConflictUseCase {
|
||||
return CheckOrderConflictUseCase(customerOrderRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideRestoreActiveOrdersUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
): RestoreActiveOrdersUseCase {
|
||||
return RestoreActiveOrdersUseCase(customerOrderRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideValidateLocalOrderHintUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
onboardingRepository: OnboardingRepository,
|
||||
): ValidateLocalOrderHintUseCase {
|
||||
return ValidateLocalOrderHintUseCase(customerOrderRepository, onboardingRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideIssueAdditionalCardUseCase(
|
||||
customerOffersRepository: CustomerOffersRepository,
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
): IssueAdditionalCardUseCase {
|
||||
return IssueAdditionalCardUseCase(customerOffersRepository, customerOrderRepository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,12 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.hasAccountData
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -19,16 +22,14 @@ import com.tangem.domain.pay.model.CustomerInfo
|
|||
import com.tangem.domain.pay.model.OrderData
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -39,7 +40,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||
|
||||
private const val TAG = "PaymentAccountStatusFetcher"
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -50,6 +51,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
private val reissueCardRepository: TangemPayReissueCardRepository,
|
||||
private val singleQuoteSupplier: SingleQuoteStatusSupplier,
|
||||
private val closeCardRepository: TangemPayCloseCardRepository,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
|
@ -149,8 +152,19 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue {
|
||||
return onboardingRepository.getCustomerInfo(account.userWalletId).fold(
|
||||
ifLeft = { error ->
|
||||
logger.e("proceedWithoutOrder ${account.userWalletId} error: $error")
|
||||
error.mapToPaymentAccountStatus(account.userWalletId)
|
||||
val cache = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId)
|
||||
if (cache != null && cache.value.hasAccountData()) {
|
||||
cache.value.copySealed(
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
error = when (error) {
|
||||
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
|
||||
else -> PaymentAccountStatusValue.Error.Unavailable
|
||||
},
|
||||
)
|
||||
} else {
|
||||
logger.e("proceedWithoutOrder ${account.userWalletId} error: $error")
|
||||
error.mapToPaymentAccountStatus(account.userWalletId)
|
||||
}
|
||||
},
|
||||
ifRight = { customerInfo ->
|
||||
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
|
||||
|
|
@ -265,8 +279,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
val quotesData = singleQuoteSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID),
|
||||
)?.value as? QuoteStatus.Data
|
||||
val cardInfo = this.cardInfo
|
||||
val productInstance = this.productInstance
|
||||
|
||||
val isDeactivated = productInstance?.status == CustomerInfo.ProductInstance.Status.DEACTIVATED
|
||||
val isFormer = state == CustomerInfo.State.FORMER
|
||||
|
|
@ -281,19 +293,26 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
}
|
||||
fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) -> {
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() &&
|
||||
(isDeactivated || isFormer) -> {
|
||||
PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
availableForWithdrawal = availableForWithdrawal.orZero(),
|
||||
),
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
cards.isNotEmpty() && productInstances.isNotEmpty() &&
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
productInstance = productInstance,
|
||||
cardInfo = cardInfo,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
|
|
@ -301,50 +320,85 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun convertToContentState(
|
||||
/**
|
||||
* Builds the [PaymentAccountStatusValue.Loaded] content state with the full list of cards.
|
||||
* Each card is the join of a product instance with its card info by `cardId`; balances are
|
||||
* payment-account-level (shared across cards). Falls back to [PaymentAccountStatusValue.IssuingCard]
|
||||
* when no card has both a product instance and card info yet (e.g. issuance in progress).
|
||||
*/
|
||||
private suspend fun CustomerInfo.convertToContentState(
|
||||
userWalletId: UserWalletId,
|
||||
productInstance: CustomerInfo.ProductInstance,
|
||||
cardInfo: CustomerInfo.CardInfo,
|
||||
fiatBalance: PaymentAccountStatusValue.FiatBalance,
|
||||
cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
|
||||
customerId: String,
|
||||
fiatRate: BigDecimal?,
|
||||
): PaymentAccountStatusValue {
|
||||
val reissueOrder = reissueCardRepository.getReissueOrderInfo(
|
||||
userWalletId = userWalletId,
|
||||
cardId = productInstance.cardId,
|
||||
).getOrNull()
|
||||
val cardsById = cards.associateBy { it.cardId }
|
||||
val tangemPayCards = productInstances.mapNotNull { productInstance ->
|
||||
val cardInfo = cardsById[productInstance.cardId] ?: return@mapNotNull null
|
||||
val cardId = productInstance.cardId
|
||||
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(cardId)
|
||||
TangemPayCard(
|
||||
id = cardId,
|
||||
productInstanceId = productInstance.id,
|
||||
cardStatus = cardInfo.cardStatus,
|
||||
hasPinCode = cardInfo.isPinSet,
|
||||
displayName = productInstance.displayName,
|
||||
limit = TangemPayCardLimitData(
|
||||
actualCardLimit = productInstance.actualCardLimit,
|
||||
adminCardLimit = productInstance.adminCardLimit,
|
||||
),
|
||||
frozenState = if (cardFrozenState == TangemPayCardFrozenState.Pending) {
|
||||
TangemPayCardFrozenState.Pending
|
||||
} else {
|
||||
productInstance.frozenState
|
||||
},
|
||||
lastDigits = cardInfo.lastFourDigits,
|
||||
state = getCardState(cardId, userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
val isReissuing = reissueOrder != null &&
|
||||
reissueOrder.orderStatus != OrderStatus.CANCELED &&
|
||||
reissueOrder.orderStatus != OrderStatus.COMPLETED
|
||||
if (tangemPayCards.isEmpty()) return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
|
||||
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
currencyCode = cardInfo.currencyCode,
|
||||
depositAddress = cardInfo.depositAddress,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
availableForWithdrawal = cardInfo.availableForWithdrawal,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
depositAddress = cryptoBalance.depositAddress,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = fiatRate,
|
||||
cards = listOf(
|
||||
TangemPayCard(
|
||||
id = productInstance.cardId,
|
||||
hasPinCode = cardInfo.isPinSet,
|
||||
displayName = productInstance.displayName,
|
||||
limit = TangemPayCardLimitData(
|
||||
actualCardLimit = productInstance.actualCardLimit,
|
||||
adminCardLimit = productInstance.adminCardLimit,
|
||||
),
|
||||
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
|
||||
lastDigits = cardInfo.lastFourDigits,
|
||||
isReissuing = isReissuing,
|
||||
),
|
||||
cards = tangemPayCards,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
availableForWithdrawal = availableForWithdrawal.orZero(),
|
||||
),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
|
||||
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
|
||||
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()
|
||||
return if (closingOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isTerminal) {
|
||||
closeCardRepository.setCloseOrderId(cardId, null)
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Closing
|
||||
}
|
||||
} else if (reissueOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isTerminal) {
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Reissuing
|
||||
}
|
||||
} else {
|
||||
TangemPayCardState.Active
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
return when (this) {
|
||||
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.pay.util.OrderStatusConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.CloseCardRequest
|
||||
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultCloseCardRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayCloseCardStore: TangemPayCloseCardStore,
|
||||
) : TangemPayCloseCardRepository {
|
||||
|
||||
override suspend fun closeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<VisaApiError, TangemPayOrderInfo> = either {
|
||||
val response = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.closeCard(
|
||||
authHeader = authHeader,
|
||||
body = CloseCardRequest(cardId = cardId),
|
||||
)
|
||||
}.bind()
|
||||
TangemPayOrderInfo(
|
||||
orderId = response.result.orderId,
|
||||
orderStatus = OrderStatusConverter.convert(response.result.status),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit> =
|
||||
runSuspendCatching {
|
||||
tangemPayCloseCardStore.setCloseOrderId(cardId, orderId)
|
||||
}.fold(
|
||||
onSuccess = { Unit.right() },
|
||||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> =
|
||||
either {
|
||||
runSuspendCatching { tangemPayCloseCardStore.getOrderId(cardId) }.getOrNull()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue