Updated on 2026-08-14
This commit is contained in:
parent
c6aa63a44f
commit
1f1f7adef4
5 changed files with 301 additions and 7 deletions
|
|
@ -13,6 +13,8 @@ import com.tangem.domain.models.account.*
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -37,16 +39,23 @@ internal class DefaultAccountsCRUDRepository(
|
|||
}
|
||||
|
||||
override suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount> = option {
|
||||
ArchivedAccount(
|
||||
accountId = accountId,
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = DerivationIndex(value = 1000).getOrNull()!!,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
createMockArchivedAccount(userWalletId = accountId.userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>> = option {
|
||||
listOf(
|
||||
createMockArchivedAccount(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>> {
|
||||
return flow {
|
||||
getArchivedAccountsSync(userWalletId).getOrNull().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit
|
||||
|
||||
override suspend fun saveAccounts(accountList: AccountList) {
|
||||
runtimeStore.update(emptyList()) {
|
||||
it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId }
|
||||
|
|
@ -62,4 +71,20 @@ internal class DefaultAccountsCRUDRepository(
|
|||
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return userWalletsStore.getSyncStrict(userWalletId)
|
||||
}
|
||||
|
||||
private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount {
|
||||
val derivationIndex = DerivationIndex(value = 1000).getOrNull()!!
|
||||
|
||||
return ArchivedAccount(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = derivationIndex,
|
||||
),
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = derivationIndex,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,12 @@ tasks.withType<Test>().configureEach {
|
|||
|
||||
dependencies {
|
||||
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.wallets.models)
|
||||
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository interface for performing CRUD operations on accounts
|
||||
|
|
@ -38,6 +39,28 @@ interface AccountsCRUDRepository {
|
|||
*/
|
||||
suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount>
|
||||
|
||||
/**
|
||||
* Retrieves a list of archived accounts associated with a specific user wallet
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
* @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not
|
||||
*/
|
||||
suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>>
|
||||
|
||||
/**
|
||||
* Provides a flow of archived accounts associated with a specific user wallet
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>>
|
||||
|
||||
/**
|
||||
* Fetches archived accounts for a specific user wallet and updates the repository
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
suspend fun fetchArchivedAccounts(userWalletId: UserWalletId)
|
||||
|
||||
/**
|
||||
* Saves a list of accounts to the repository
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
typealias ArchivedAccountList = List<ArchivedAccount>
|
||||
|
||||
/**
|
||||
* Use case for retrieving archived accounts for a specific user wallet
|
||||
*
|
||||
* @property crudRepository the repository for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class GetArchivedAccountsUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Executes the use case to retrieve archived accounts for the given user wallet
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
operator fun invoke(userWalletId: UserWalletId): LceFlow<Throwable, ArchivedAccountList> = channelFlow {
|
||||
val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId)
|
||||
|
||||
archivedAccounts
|
||||
.onRight { send(it.lceContent()) }
|
||||
.onLeft {
|
||||
send(lceLoading())
|
||||
|
||||
launch {
|
||||
fetchArchivedAccounts(userWalletId).getOrElse {
|
||||
send(it.lceError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribeOnArchivedAccounts(userWalletId)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, ArchivedAccountList> {
|
||||
return Either.catch {
|
||||
crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse {
|
||||
error("Archived accounts not found for user wallet: $userWalletId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, Unit> {
|
||||
return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) }
|
||||
}
|
||||
|
||||
private suspend fun ProducerScope<Lce<Throwable, ArchivedAccountList>>.subscribeOnArchivedAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
) {
|
||||
crudRepository.getArchivedAccounts(userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.retryWhen { cause, _ ->
|
||||
send(cause.lceError())
|
||||
|
||||
delay(timeMillis = 2000)
|
||||
|
||||
true
|
||||
}
|
||||
.collectLatest { archivedAccounts ->
|
||||
send(archivedAccounts.lceContent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
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)
|
||||
class GetArchivedAccountsUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = GetArchivedAccountsUseCase(crudRepository)
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should emit archived accounts when repository returns data`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccounts = listOf(
|
||||
mockk<ArchivedAccount>(),
|
||||
mockk<ArchivedAccount>(),
|
||||
)
|
||||
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption()
|
||||
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(useCase(userWalletId))
|
||||
|
||||
// Assert
|
||||
val expected = listOf(archivedAccounts.lceContent())
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getArchivedAccountsSync(userWalletId)
|
||||
crudRepository.getArchivedAccounts(userWalletId)
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should emit loading and fetch when accounts not found`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccounts = listOf(
|
||||
mockk<ArchivedAccount>(),
|
||||
mockk<ArchivedAccount>(),
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
|
||||
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(useCase(userWalletId))
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
lceLoading(),
|
||||
archivedAccounts.lceContent(),
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
crudRepository.getArchivedAccountsSync(userWalletId)
|
||||
crudRepository.fetchArchivedAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccounts(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = IllegalStateException("Test error")
|
||||
val archivedAccounts = listOf(
|
||||
mockk<ArchivedAccount>(),
|
||||
mockk<ArchivedAccount>(),
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception
|
||||
every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(useCase(userWalletId))
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
lceLoading(),
|
||||
archivedAccounts.lceContent(),
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
crudRepository.getArchivedAccountsSync(userWalletId)
|
||||
crudRepository.fetchArchivedAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccounts(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = IllegalStateException("Fetch error")
|
||||
|
||||
coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None
|
||||
every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow()
|
||||
coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(useCase(userWalletId))
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
lceLoading(),
|
||||
exception.lceError(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
crudRepository.getArchivedAccountsSync(userWalletId)
|
||||
crudRepository.fetchArchivedAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccounts(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> TestScope.getEmittedValues(flow: Flow<T>): List<T> {
|
||||
val values = mutableListOf<T>()
|
||||
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
flow.toList(values)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue