Updated on 2026-08-14
This commit is contained in:
parent
bb4afdec80
commit
16b33742ca
9 changed files with 686 additions and 12 deletions
|
|
@ -119,9 +119,6 @@ interface TangemTechApi {
|
|||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PATCH("v1/user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// region user-wallets
|
||||
|
|
@ -148,12 +145,15 @@ interface TangemTechApi {
|
|||
|
||||
// region account
|
||||
@GET("/v1/wallets/{walletId}/accounts")
|
||||
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse<GetWalletAccountsResponse>
|
||||
suspend fun getWalletAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-None-Match") eTag: String? = null,
|
||||
): ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/accounts")
|
||||
suspend fun saveWalletAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") ifMatch: String,
|
||||
@Header("If-Match") eTag: String,
|
||||
@Body body: SaveWalletAccountsResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
suspend fun <T> DataStore<T>.getSyncOrNull(): T? = data.firstOrNull()
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
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.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.IF_NONE_MATCH_HEADER
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Default implementation of [WalletAccountsFetcher] and [WalletAccountsSaver]
|
||||
*
|
||||
* @property tangemTechApi API for network requests
|
||||
* @property accountsResponseStoreFactory factory to create [AccountsResponseStore]
|
||||
* @property userTokensSaver saves user tokens to the database
|
||||
* @property fetchWalletAccountsErrorHandler handles errors during fetching wallet accounts
|
||||
* @property eTagsStore store for ETags to manage caching
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultWalletAccountsFetcher(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletAccountsFetcher, WalletAccountsSaver {
|
||||
|
||||
override suspend fun fetch(userWalletId: UserWalletId) {
|
||||
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
|
||||
val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
val unassignedTokens = accountsResponse?.unassignedTokens
|
||||
|
||||
if (!unassignedTokens.isNullOrEmpty()) {
|
||||
assignTokens(userWalletId, accountsResponse)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
push(userWalletId = userWalletId, accounts = response.accounts)
|
||||
store(userWalletId = userWalletId, response = response)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
val store = getAccountsResponseStore(userWalletId = userWalletId)
|
||||
|
||||
store.updateData { response }
|
||||
}
|
||||
|
||||
override suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) {
|
||||
push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts))
|
||||
}
|
||||
|
||||
override suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
var eTag = getETag(userWalletId)
|
||||
|
||||
if (eTag == null) {
|
||||
fetch(userWalletId)
|
||||
|
||||
eTag = getETag(userWalletId) ?: error("ETag is null after fetch")
|
||||
}
|
||||
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
|
||||
saveETag(userWalletId, apiResponse)
|
||||
|
||||
apiResponse.bind()
|
||||
},
|
||||
onError = { error ->
|
||||
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchWalletAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
): GetWalletAccountsResponse? {
|
||||
return safeApiCall(
|
||||
call = {
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.getWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = getETag(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
saveETag(userWalletId, apiResponse)
|
||||
|
||||
val responseBody = apiResponse.bind()
|
||||
store(userWalletId = userWalletId, response = responseBody)
|
||||
|
||||
responseBody
|
||||
},
|
||||
onError = {
|
||||
// pushWalletAccounts and storeWalletAccounts help to avoid cyclic dependency
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = it,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = ::push,
|
||||
storeWalletAccounts = ::store,
|
||||
)
|
||||
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
|
||||
val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId)
|
||||
|
||||
pushAndStore(userWalletId = userWalletId, response = accountsResponseWithTokens)
|
||||
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = accountsResponseWithTokens.toUserTokensResponse(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getETag(userWalletId: UserWalletId): String? {
|
||||
return eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
}
|
||||
|
||||
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
|
||||
val eTag = apiResponse.headers[IF_NONE_MATCH_HEADER]?.firstOrNull()
|
||||
|
||||
if (eTag != null) {
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore {
|
||||
return accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -100,13 +100,13 @@ internal class DefaultAccountsCRUDRepository(
|
|||
saveAccountsMutex.withLock {
|
||||
val store = getAccountsResponseStore(userWalletId = accountList.userWallet.walletId)
|
||||
|
||||
val version = store.data.firstOrNull()?.wallet?.version ?: 0
|
||||
store.data.firstOrNull()?.wallet?.version ?: 0
|
||||
val body = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = accountList.userWallet.walletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
eTag = "", // TODO("[REDACTED_JIRA]")
|
||||
body = body,
|
||||
)
|
||||
.getOrThrow()
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ internal fun createGetWalletAccountsResponse(
|
|||
iconColor: String? = null,
|
||||
derivationIndex: Int? = null,
|
||||
tokens: List<UserTokensResponse.Token>? = emptyList(),
|
||||
unassignedTokens: List<UserTokensResponse.Token> = emptyList(),
|
||||
): GetWalletAccountsResponse {
|
||||
return GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
|
|
@ -66,7 +67,7 @@ internal fun createGetWalletAccountsResponse(
|
|||
)
|
||||
.let(::add)
|
||||
},
|
||||
unassignedTokens = emptyList(),
|
||||
unassignedTokens = unassignedTokens,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,460 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
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.IF_NONE_MATCH_HEADER
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
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.SaveWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultWalletAccountsFetcherTest {
|
||||
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
|
||||
private val accountsResponseStore: AccountsResponseStore = mockk()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler = mockk(relaxUnitFun = true)
|
||||
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
|
||||
|
||||
private val fetcher: DefaultWalletAccountsFetcher = DefaultWalletAccountsFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
userTokensSaver = userTokensSaver,
|
||||
fetchWalletAccountsErrorHandler = fetchWalletAccountsErrorHandler,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val eTag = "etag"
|
||||
|
||||
@BeforeAll
|
||||
fun setUp() {
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
|
||||
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
userTokensSaver,
|
||||
fetchWalletAccountsErrorHandler,
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = null
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Fetch {
|
||||
|
||||
@Test
|
||||
fun `fetch should call assignTokens when unassignedTokens are not empty`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val unassignedToken = createToken(accountId = null)
|
||||
|
||||
val accountsResponse = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val updatedAccountsResponse = accountsResponse.copy(
|
||||
accounts = accountsResponse.accounts.map {
|
||||
it.copy(tokens = listOf(unassignedToken.copy(accountId = accountId)))
|
||||
},
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
} returns apiResponse
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns accountsResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
|
||||
// Act
|
||||
fetcher.fetch(userWalletId)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.data
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = any(),
|
||||
userWalletId = any(),
|
||||
savedAccountsResponse = any(),
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch should not call assignTokens when unassignedTokens are empty`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val accountsResponse = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
} returns apiResponse
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns accountsResponse
|
||||
|
||||
// Act
|
||||
fetcher.fetch(userWalletId)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.data
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = any(),
|
||||
userWalletId = any(),
|
||||
savedAccountsResponse = any(),
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
|
||||
tangemTechApi.saveWalletAccounts(walletId = any(), eTag = any(), body = any())
|
||||
userTokensSaver.push(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val apiError = ApiResponse.Error(ApiResponseError.NetworkException)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
} returns apiError as ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
// Act
|
||||
fetcher.fetch(userWalletId)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.data
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = apiError.cause,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
eTagsStore.store(userWalletId = any(), key = any(), value = any())
|
||||
tangemTechApi.saveWalletAccounts(walletId = any(), eTag = any(), body = any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `push should throw error when saveWalletAccounts returns PRECONDITION_FAILED`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val unassignedToken = createToken(accountId = null)
|
||||
|
||||
val accountsResponse = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
|
||||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
val apiError = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.PRECONDITION_FAILED,
|
||||
message = null,
|
||||
errorBody = null,
|
||||
)
|
||||
val saveApiResponse = ApiResponse.Error(apiError)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val updatedAccountsResponse = accountsResponse.copy(
|
||||
accounts = accountsResponse.accounts.map {
|
||||
it.copy(tokens = listOf(unassignedToken.copy(accountId = accountId)))
|
||||
},
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
} returns apiResponse
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns accountsResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
} returns saveApiResponse as ApiResponse<Unit>
|
||||
|
||||
// Act
|
||||
val actual = runCatching { fetcher.fetch(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(apiError)
|
||||
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.data
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = any(),
|
||||
userWalletId = any(),
|
||||
savedAccountsResponse = any(),
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Store {
|
||||
|
||||
@Test
|
||||
fun `store should update data in AccountsResponseStore`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(userWalletId = userWalletId)
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns response
|
||||
|
||||
// Act
|
||||
fetcher.store(userWalletId = userWalletId, response = response)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Push {
|
||||
|
||||
@Test
|
||||
fun `push should call saveWalletAccounts with correct params`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
)
|
||||
val response = SaveWalletAccountsResponse(accounts)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = response,
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
|
||||
// Act
|
||||
fetcher.push(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = response,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `push should throw error when saveWalletAccounts returns PRECONDITION_FAILED`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
)
|
||||
val response = SaveWalletAccountsResponse(accounts)
|
||||
val apiError = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.PRECONDITION_FAILED,
|
||||
message = null,
|
||||
errorBody = null,
|
||||
)
|
||||
val saveApiResponse = ApiResponse.Error(apiError)
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = response,
|
||||
)
|
||||
} returns saveApiResponse as ApiResponse<Unit>
|
||||
|
||||
// Act
|
||||
val actual = runCatching { fetcher.push(userWalletId, response) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(apiError)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class PushAndStore {
|
||||
|
||||
@Test
|
||||
fun `pushAndStore should call push and store with correct params`() = runTest {
|
||||
// Arrange
|
||||
listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
)
|
||||
val response = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(accounts = response.accounts),
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
|
||||
|
||||
// Act
|
||||
fetcher.pushAndStore(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(accounts = response.accounts),
|
||||
)
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createToken(
|
||||
networkId: String = "ethereum",
|
||||
derivationPath: String = "m/44'/60'/0'/0/0",
|
||||
list: List<String> = emptyList(),
|
||||
name: String = "Ethereum",
|
||||
symbol: String = "ETH",
|
||||
decimals: Int = 18,
|
||||
contractAddress: String? = null,
|
||||
id: String? = null,
|
||||
accountId: String? = null,
|
||||
) = UserTokensResponse.Token(
|
||||
id = id,
|
||||
accountId = accountId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
contractAddress = contractAddress,
|
||||
addresses = list,
|
||||
)
|
||||
}
|
||||
|
|
@ -585,7 +585,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
eTag = "",
|
||||
body = body,
|
||||
)
|
||||
} returns apiResponse
|
||||
|
|
@ -609,7 +609,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body)
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, "", body)
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet)
|
||||
converter.convert(accountList)
|
||||
accountsResponseStore.updateData(any())
|
||||
|
|
@ -638,7 +638,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
eTag = "",
|
||||
body = body,
|
||||
)
|
||||
} returns apiResponse
|
||||
|
|
@ -652,7 +652,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body)
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, "", body)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.data.common.account
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Component for fetching wallet accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface WalletAccountsFetcher {
|
||||
|
||||
/** Fetch wallet accounts by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun fetch(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.common.account
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Saver for wallet accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface WalletAccountsSaver {
|
||||
|
||||
/** Push and store wallet accounts [response] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
|
||||
|
||||
/** Store wallet accounts [response] by [userWalletId] */
|
||||
suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
|
||||
|
||||
/** Push wallet accounts [body] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse)
|
||||
|
||||
/** Push wallet accounts [accounts] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue