Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-23 16:37:57 +04:00
parent b9bb9e1086
commit 058a6ab4d5
15 changed files with 969 additions and 256 deletions

View file

@ -1,18 +1,18 @@
package com.tangem.data.quotes.multi
import arrow.core.Either
import com.tangem.data.common.api.safeApiCallWithTimeout
import arrow.core.getOrElse
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.data.common.quote.QuotesFetcher.Field
import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.data.quotes.store.setSourceAsCache
import com.tangem.data.quotes.store.setSourceAsOnlyCache
import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -20,16 +20,16 @@ import javax.inject.Singleton
/**
* Default implementation of [MultiQuoteStatusFetcher]
*
* @property tangemTechApi tangemTech api
* @property quotesFetcher quotes fetcher
* @property appCurrencyResponseStore app currency response store
* @property quotesStatusesStore quotes store
* @property quotesStatusesStore quotes statuses store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val quotesFetcher: QuotesFetcher,
private val appCurrencyResponseStore: AppCurrencyResponseStore,
private val quotesStatusesStore: QuotesStatusesStore,
private val dispatchers: CoroutineDispatcherProvider,
@ -51,16 +51,13 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
)
val appCurrencyId = getAppCurrencyId(params = params)
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
val response = safeApiCallWithTimeout(
call = {
withContext(dispatchers.io) {
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind()
}
},
onError = { error -> throw error },
val response = quotesFetcher.fetch(
fiatCurrencyId = appCurrencyId,
currenciesIds = replacementIdsResult.idsForRequest,
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H),
)
.getOrElse { error("Cause: $it") }
val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies(
response = response,

View file

@ -1,107 +1,125 @@
package com.tangem.data.quotes.multi
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
import com.tangem.common.test.utils.assertEither
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.data.quotes.store.setSourceAsCache
import com.tangem.data.quotes.store.setSourceAsOnlyCache
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultMultiQuoteStatusFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
private val quotesFetcher = mockk<QuotesFetcher>()
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>()
private val quotesStore = mockk<QuotesStatusesStore>(relaxed = true)
private val fetcher = DefaultMultiQuoteStatusFetcher(
tangemTechApi = tangemTechApi,
quotesFetcher = quotesFetcher,
appCurrencyResponseStore = appCurrencyResponseStore,
quotesStatusesStore = quotesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@BeforeEach
fun resetMocks() {
clearMocks(quotesFetcher, appCurrencyResponseStore, quotesStore)
}
@Test
fun `fetch quotes successfully`() = runTest {
fun `fetch successfully`() = runTest {
// Arrange
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC,ETH"
coEvery {
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val currenciesIds = setOf("BTC", "ETH")
coEvery {
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
} returns successResponse.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
quotesStore.store(values = successResponse.quotes)
}
coVerify(inverse = true) {
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest {
fun `fetch successfully if currenciesIds from params is empty`() = runTest {
// Arrange
val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null)
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerify(inverse = true) {
quotesStore.setSourceAsCache(currenciesIds = any())
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any())
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest {
fun `fetch successfully if appCurrencyId from params is not null`() = runTest {
// Arrange
val appCurrencyId = "usd"
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
val coinIds = "BTC,ETH"
coEvery {
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val currenciesIds = setOf("BTC", "ETH")
coEvery {
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
} returns successResponse.right()
// Act
val actual = fetcher(params)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
quotesStore.store(values = successResponse.quotes)
}
@ -109,17 +127,22 @@ internal class DefaultMultiQuoteStatusFetcherTest {
appCurrencyResponseStore.getSyncOrNull()
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest {
fun `fetch failure because appCurrencyId from params is blank`() = runTest {
// Arrange
val appCurrencyId = ""
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
// Act
val actual = fetcher(params)
// Assert
val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left()
assertEither(actual, expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
@ -127,55 +150,59 @@ internal class DefaultMultiQuoteStatusFetcherTest {
coVerify(inverse = true) {
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
.isEqualTo("Unable to get AppCurrency for updating quotes")
}
@Test
fun `fetch quotes failure because api request failed`() = runTest {
fun `fetch failure because api request failed`() = runTest {
// Arrange
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
val currenciesIds = setOf("BTC", "ETH")
val error = QuotesFetcher.Error.ApiOperationError(ApiResponseError.NetworkException)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC,ETH"
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
coEvery {
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
} returns error.left()
// Act
val actual = fetcher(params)
// Assert
val expected = IllegalStateException("Cause: ApiOperationError(apiError=NetworkException)").left()
assertEither(actual, expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
}
coVerify(inverse = true) {
quotesStore.store(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
@Test
fun `fetch quotes failure because app currency not found`() = runTest {
fun `fetch failure because app currency not found`() = runTest {
// Arrange
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
// Act
val actual = fetcher(params)
// Assert
val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left()
assertEither(actual, expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
appCurrencyResponseStore.getSyncOrNull()
@ -183,11 +210,9 @@ internal class DefaultMultiQuoteStatusFetcherTest {
}
coVerify(inverse = true) {
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
private companion object {
@ -212,5 +237,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
"ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN),
),
)
val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H)
}
}

View file

@ -1,176 +1,79 @@
package com.tangem.data.quotes.single
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher
import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.data.quotes.store.setSourceAsCache
import com.tangem.data.quotes.store.setSourceAsOnlyCache
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultSingleQuoteStatusFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
private val quotesStore = mockk<QuotesStatusesStore>(relaxed = true)
private val multiFetcher = DefaultMultiQuoteStatusFetcher(
tangemTechApi = tangemTechApi,
appCurrencyResponseStore = appCurrencyResponseStore,
quotesStatusesStore = quotesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val multiFetcher = mockk<MultiQuoteStatusFetcher>()
private val singleFetcher = DefaultSingleQuoteStatusFetcher(multiFetcher)
@Test
fun `fetch single quote successfully`() = runTest {
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.store(values = successResponse.quotes)
}
coVerify(inverse = true) {
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
@BeforeEach
fun resetMocks() {
clearMocks(multiFetcher)
}
@Test
fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest {
val appCurrencyId = "usd"
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
fun `fetch successfully if multiFetcher returns success`() = runTest {
// Arrange
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null)
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val multiFetcherParams = MultiQuoteStatusFetcher.Params(
currenciesIds = setOf(params.rawCurrencyId),
appCurrencyId = params.appCurrencyId,
)
coEvery { multiFetcher(multiFetcherParams) } returns Unit.right()
// Act
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.store(values = successResponse.quotes)
}
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual.isRight()).isTrue()
coVerify(exactly = 1) { multiFetcher(multiFetcherParams) }
}
@Test
fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest {
val appCurrencyId = ""
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
fun `fetch failure if multiFetcher returns erro`() = runTest {
// Arrange
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currencyId, appCurrencyId = null)
val multiFetcherParams = MultiQuoteStatusFetcher.Params(
currenciesIds = setOf(params.rawCurrencyId),
appCurrencyId = params.appCurrencyId,
)
val multiFetcherError = IllegalStateException("").left()
coEvery { multiFetcher(multiFetcherParams) } returns multiFetcherError
// Act
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
}
// Assert
val expected = multiFetcherError
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
.isEqualTo("Unable to get AppCurrency for updating quotes")
}
@Test
fun `fetch single quote failure because api request failed`() = runTest {
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
quotesStore.store(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
@Test
fun `fetch single quote failure because app currency not found`() = runTest {
val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesStore.store(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
coVerify(exactly = 1) { multiFetcher(multiFetcherParams) }
}
private companion object {
val currenciesId = CryptoCurrency.RawID(value = "BTC")
val usdAppCurrency = CurrenciesResponse.Currency(
id = "USD".lowercase(),
code = "USD",
name = "US Dollar",
unit = "$",
type = "fiat",
rateBTC = "",
)
val successResponse = QuotesResponse(
quotes = mapOf(
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
),
)
val currencyId = CryptoCurrency.RawID(value = "BTC")
}
}