diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 9a557cd922..f98dfae648 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -119,9 +119,6 @@ interface TangemTechApi { @Path("application_id") applicationId: String, @Body body: NotificationApplicationCreateBody, ): ApiResponse - - @PATCH("v1/user-wallets/wallets/{wallet_id}/notify") - suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // 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 + suspend fun getWalletAccounts( + @Path("walletId") walletId: String, + @Header("If-None-Match") eTag: String? = null, + ): ApiResponse @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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/DataStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/DataStoreExt.kt new file mode 100644 index 0000000000..a8ecd2a3e2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/DataStoreExt.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.utils + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.firstOrNull + +suspend fun DataStore.getSyncOrNull(): T? = data.firstOrNull() \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt new file mode 100644 index 0000000000..45a16f947b --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -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) { + 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) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 333001ed0e..73a4766449 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -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() diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt index 16dfff6ac7..880b20520e 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -46,6 +46,7 @@ internal fun createGetWalletAccountsResponse( iconColor: String? = null, derivationIndex: Int? = null, tokens: List? = emptyList(), + unassignedTokens: List = emptyList(), ): GetWalletAccountsResponse { return GetWalletAccountsResponse( wallet = GetWalletAccountsResponse.Wallet( @@ -66,7 +67,7 @@ internal fun createGetWalletAccountsResponse( ) .let(::add) }, - unassignedTokens = emptyList(), + unassignedTokens = unassignedTokens, ) } diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt new file mode 100644 index 0000000000..d90aa7727c --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -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(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 + + // 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 + + // 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(), + ) + 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(), + ) + 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 + + // 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(), + ) + 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 = 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, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 8fbcf92aea..6c620a5b8a 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -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) { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt new file mode 100644 index 0000000000..ba81c5db5a --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt @@ -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) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt new file mode 100644 index 0000000000..f4b684613b --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt @@ -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) +} \ No newline at end of file