Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-05 14:01:26 +03:00
commit 9aacdb2ed3
695 changed files with 22022 additions and 5116 deletions

View file

@ -77,7 +77,7 @@ internal fun createAccountList(
): AccountList {
return AccountList(
userWalletId = userWalletId,
accounts = setOf(createCryptoPortfolio(userWalletId)),
accounts = listOf(createCryptoPortfolio(userWalletId)),
totalAccounts = 1,
sortType = sortType,
groupType = groupType,

View file

@ -2,8 +2,6 @@ package com.tangem.data.account.converter
import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWalletId
import org.junit.jupiter.api.Test
@ -16,13 +14,7 @@ class SaveWalletAccountsResponseConverterTest {
fun convert() {
// Arrange
val userWalletId = UserWalletId("011")
val accountList = AccountList(
userWalletId = userWalletId,
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)),
totalAccounts = 1,
)
.getOrNull()!!
val accountList = createAccountList(userWalletId)
// Act
val actual = SaveWalletAccountsResponseConverter.convert(value = accountList)

View file

@ -29,6 +29,8 @@ class DefaultSingleAccountListFetcherTest {
// Arrange
val params = SingleAccountListFetcher.Params(userWalletId = userWalletId)
coEvery { walletAccountsFetcher.fetch(userWalletId) } returns mockk()
// Act
val actual = fetcher.invoke(params)

View file

@ -199,7 +199,7 @@ class DefaultWalletAccountsFetcherTest {
@Test
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
// Arrange
val savedAccountsResponse = null
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val apiError = ApiResponse.Error(ApiResponseError.NetworkException())
accountsResponseStoreFlow.value = savedAccountsResponse
@ -212,7 +212,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)
@ -231,7 +231,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)

View file

@ -3,15 +3,21 @@ package com.tangem.data.account.fetcher
import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.OnlyWalletIdBody
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
@ -27,14 +33,19 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class FetchWalletAccountsErrorHandlerTest {
private val tangemTechApi: TangemTechApi = mockk()
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
private val handler = FetchWalletAccountsErrorHandler(
tangemTechApi = tangemTechApi,
userTokensSaver = userTokensSaver,
userTokensResponseStore = userTokensResponseStore,
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
eTagsStore = eTagsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
@ -53,6 +64,8 @@ class FetchWalletAccountsErrorHandlerTest {
@Test
fun `does not update accounts when response is up to date`() = runTest {
// Arrange
val response = createGetWalletAccountsResponse(userWalletId)
val error = ApiResponseError.HttpException(
code = Code.NOT_MODIFIED,
message = "Not Modified",
@ -63,7 +76,7 @@ class FetchWalletAccountsErrorHandlerTest {
handler.handle(
error = error,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = response,
pushWalletAccounts = pushWalletAccounts,
storeWalletAccounts = storeWalletAccounts,
)
@ -91,6 +104,16 @@ class FetchWalletAccountsErrorHandlerTest {
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val eTagValue = "etag-value"
val apiResponse = ApiResponse.Success(
data = Unit,
headers = mapOf(ETAG_HEADER to listOf(eTagValue)),
code = Code.CREATED,
)
coEvery {
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
} returns apiResponse
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
// Act
@ -105,6 +128,8 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify {
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
pushWalletAccounts(userWalletId, listOf(accountDTO))
storeWalletAccounts(userWalletId, savedAccountsResponse)
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.account.repository
import android.content.res.Resources
import arrow.core.None
import arrow.core.toOption
import com.google.common.truth.Truth
@ -10,7 +11,6 @@ import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -54,12 +54,14 @@ class DefaultAccountsCRUDRepositoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensSaver: UserTokensSaver = mockk()
private val eTagsStore: ETagsStore = mockk()
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>> = mockk(relaxUnitFun = true)
private val convertersContainer: AccountConverterFactoryContainer = mockk()
private val accountListConverter: AccountListConverter = mockk()
private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk()
private val resources: Resources = mockk()
private val repository = DefaultAccountsCRUDRepository(
tangemTechApi = tangemTechApi,
walletAccountsSaver = walletAccountsSaver,
@ -67,8 +69,9 @@ class DefaultAccountsCRUDRepositoryTest {
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
archivedAccountsETagStore = archivedAccountsETagStore,
convertersContainer = convertersContainer,
resources = resources,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -532,7 +535,7 @@ class DefaultAccountsCRUDRepositoryTest {
val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO)
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery {
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
@ -546,9 +549,10 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(actual).containsExactly(archivedAccount)
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.getSyncOrNull()
archivedAccountsStoreFactory.create(userWalletId)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.update(any())
}
}
@ -558,7 +562,7 @@ class DefaultAccountsCRUDRepositoryTest {
val eTag = "etag123"
val exception = Exception("API error")
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } throws exception
// Act
@ -570,7 +574,7 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull()
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
archivedAccountsETagStore.getSyncOrNull()
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
}
}
@ -644,15 +648,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCountSync {
inner class GetTotalActiveAccountsCountSync {
@Test
fun `getTotalAccountsCountSync returns None if account list response is null`() = runTest {
fun `getTotalActiveAccountsCountSync returns None if account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
Truth.assertThat(actual).isEqualTo(None)
@ -664,20 +668,22 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
fun `getTotalActiveAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
// Arrange
val totalAccounts = 5
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
val expected = totalAccounts.toOption()
val expected = 1.toOption()
Truth.assertThat(actual).isEqualTo(expected)
verifyOrder {
@ -689,15 +695,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCount {
inner class GetTotalActiveAccountsCount {
@Test
fun `getTotalAccountsCount emits 0 when account list response is null`() = runTest {
fun `getTotalActiveAccountsCount emits 0 when account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
@ -710,21 +716,23 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCount emits correct value when response is valid`() = runTest {
fun `getTotalActiveAccountsCount emits correct value when response is valid`() = runTest {
// Arrange
val totalAccounts = 7
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
Truth.assertThat(actual).containsExactly(totalAccounts.toOption())
Truth.assertThat(actual).containsExactly(1.toOption())
verifyOrder {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data

View file

@ -7,10 +7,10 @@ import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
@ -183,6 +183,8 @@ class DefaultMainAccountTokensMigrationTest {
accountsResponseStoreFlow.value = response
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
// Act
val actual = migration.migrate(userWalletId, derivationIndex)
@ -199,6 +201,7 @@ class DefaultMainAccountTokensMigrationTest {
coVerifySequence {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
accountsResponseStore.updateData(any())
userTokensSaver.push(
userWalletId = userWalletId,
response = migratedResponse.toUserTokensResponse(),

View file

@ -3,12 +3,12 @@ package com.tangem.data.account.utils
import com.google.common.truth.Truth
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
@ -23,17 +23,17 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultWalletAccountsResponseFactoryTest {
private val userWalletsListRepository = mockk<UserWalletsListRepository>()
private val userWalletsStore = mockk<UserWalletsStore>()
private val cryptoPortfolioCF = mockk<CryptoPortfolioConverter.Factory>()
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
private val userTokensResponseFactory = mockk<UserTokensResponseFactory>()
private val cardCryptoCurrencyFactory = mockk<CardCryptoCurrencyFactory>()
private val networkFactory = mockk<NetworkFactory>()
private val factory = DefaultWalletAccountsResponseFactory(
userWalletsListRepository = userWalletsListRepository,
userWalletsStore = userWalletsStore,
cryptoPortfolioCF = cryptoPortfolioCF,
userTokensResponseFactory = userTokensResponseFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
networkFactory = networkFactory,
)
private val userWalletId = UserWalletId("011")
@ -46,11 +46,11 @@ class DefaultWalletAccountsResponseFactoryTest {
@AfterEach
fun tearDownEach() {
clearMocks(
userWalletsListRepository,
userWalletsStore,
cryptoPortfolioCF,
cryptoPortfolioConverter,
userTokensResponseFactory,
cardCryptoCurrencyFactory,
networkFactory,
)
}
@ -63,12 +63,12 @@ class DefaultWalletAccountsResponseFactoryTest {
tokens = emptyList(),
)
coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList()
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
} returns userTokensResponse
@ -88,11 +88,11 @@ class DefaultWalletAccountsResponseFactoryTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userWalletsStore.getSyncOrNull(userWalletId)
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
}
}
@ -104,27 +104,24 @@ class DefaultWalletAccountsResponseFactoryTest {
every { walletId } returns userWalletId
}
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = listOf(mockk(relaxed = true)),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountsDTO = createWalletAccountDTO(userWalletId)
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
@ -145,13 +142,12 @@ class DefaultWalletAccountsResponseFactoryTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
userWalletsStore.getSyncOrNull(userWalletId)
cryptoPortfolioConverter.convertListBack(accounts)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
}
}
@ -162,22 +158,26 @@ class DefaultWalletAccountsResponseFactoryTest {
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList()
// Act
@ -203,7 +203,7 @@ class DefaultWalletAccountsResponseFactoryTest {
every { walletId } returns userWalletId
}
val assignedTokens = listOf(mockk<CryptoCurrency.Token>(), mockk<CryptoCurrency.Token>())
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,

View file

@ -4,6 +4,8 @@ import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.flattenTokens
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId