From 8d5c9e0e58daa8b6c2d8ad612d956a4258ae7175 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 11:51:40 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/di/QuotesStoreModule.kt | 29 +-- .../com/tangem/utils/coroutines/JobHolder.kt | 3 + .../quotes/multi/DefaultMultiQuoteFetcher.kt | 55 ++++-- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 86 +++++++++ .../data/quotes/store/DefaultQuotesStoreV2.kt | 2 + .../tangem/data/quotes/store/QuotesStoreV2.kt | 3 + .../multi/DefaultMultiQuoteFetcherTest.kt | 77 +++++++- .../multi/DefaultMultiQuoteUpdaterTest.kt | 169 ++++++++++++++++++ .../quotes/store/QuotesStoreGetMethodTest.kt | 29 +++ .../domain/quotes/multi/MultiQuoteFetcher.kt | 11 +- .../domain/quotes/multi/MultiQuoteUpdater.kt | 15 ++ 11 files changed, 448 insertions(+), 31 deletions(-) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index dbcf34e870..48e2d14670 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -26,21 +27,27 @@ internal object QuotesStoreModule { @Provides @Singleton - fun provideQuotesStore( + fun providePersistenceQuotesStore( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, - ): QuotesStore { - return DefaultQuotesStore( - persistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithStringKeyTypes(), - defaultValue = emptyMap(), - ), - produceFile = { context.dataStoreFile(fileName = "quotes") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ): DataStore> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), ), + produceFile = { context.dataStoreFile(fileName = "quotes") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + + @Provides + @Singleton + fun provideQuotesStore(persistenceStore: DataStore>): QuotesStore { + return DefaultQuotesStore( + persistenceStore = persistenceStore, runtimeStore = RuntimeSharedStore(), ) } diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 6e5a872f02..4676289125 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -24,7 +24,10 @@ class JobHolder { /** Cancel current [job] */ fun cancel() { job?.cancel() + job = null } + + fun isEmpty() = job == null } fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt index 5fc337e08c..e9fa631d23 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -6,9 +6,13 @@ import com.tangem.data.quotes.store.QuotesStoreV2 import com.tangem.data.tokens.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.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton /** * Default implementation of [MultiQuoteFetcher] @@ -16,18 +20,26 @@ import timber.log.Timber * @property tangemTechApi tangemTech api * @property appCurrencyResponseStore app currency response store * @property quotesStore quotes store + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteFetcher( +@Singleton +internal class DefaultMultiQuoteFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, private val quotesStore: QuotesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() - override suspend fun invoke(params: MultiQuoteFetcher.Params): Either = Either.catch { + override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { + if (params.currenciesIds.isEmpty()) { + Timber.d("No currencies to fetch quotes for") + return@catchOn + } + quotesStore.refresh(currenciesIds = params.currenciesIds) val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( @@ -37,26 +49,37 @@ internal class DefaultMultiQuoteFetcher( ), ) - val appCurrency = appCurrencyResponseStore.getSyncOrNull() - ?: error(message = "Unable to get AppCurrency for updating quotes") + val appCurrencyId = getAppCurrencyId(params = params) + val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - safeApiCallWithTimeout( - call = { - val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - val response = tangemTechApi.getQuotes(currencyId = appCurrency.id, coinIds = coinIds).bind() - - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response = response, - filteredIds = replacementIdsResult.idsFiltered, - ) - - quotesStore.storeActual(values = updatedResponse.quotes) - }, + val response = safeApiCallWithTimeout( + call = { tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() }, onError = { error -> throw error }, ) + + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response = response, + filteredIds = replacementIdsResult.idsFiltered, + ) + + quotesStore.storeActual(values = updatedResponse.quotes) } .onLeft { Timber.e(it) quotesStore.storeError(currenciesIds = params.currenciesIds) } + + private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { + val appCurrencyId = params.appCurrencyId + ?: appCurrencyResponseStore.getSyncOrNull()?.id + + if (appCurrencyId.isNullOrBlank()) { + val exception = IllegalStateException("Unable to get AppCurrency for updating quotes") + Timber.e(exception) + + throw exception + } + + return appCurrencyId + } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt new file mode 100644 index 0000000000..06e492857e --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -0,0 +1,86 @@ +package com.tangem.data.quotes.multi + +import androidx.annotation.VisibleForTesting +import arrow.core.left +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes + * + * @property appCurrencyResponseStore app currency response store + * @property quotesStore quotes store + * @property multiQuoteFetcher multi quote fetcher + * @param dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class DefaultMultiQuoteUpdater @Inject constructor( + private val appCurrencyResponseStore: AppCurrencyResponseStore, + private val quotesStore: QuotesStoreV2, + private val multiQuoteFetcher: MultiQuoteFetcher, + dispatchers: CoroutineDispatcherProvider, +) : MultiQuoteUpdater { + + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default) + private val updaterHolder = JobHolder() + + override fun subscribe() { + Timber.d("Subscribe on quotes updates") + getMultiQuoteUpdates() + .launchIn(coroutineScope) + .saveIn(updaterHolder) + } + + override fun unsubscribe() { + Timber.e("Unsubscribe from quotes updates") + updaterHolder.cancel() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getMultiQuoteUpdates(): EitherFlow { + return appCurrencyResponseStore.get() + .drop(count = 1) // skip initial value + .distinctUntilChanged() + .filterNotNull() + .mapLatest { appCurrency -> + val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty() + .mapTo(destination = hashSetOf(), transform = Quote::rawCurrencyId) + + multiQuoteFetcher( + params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id), + ) + .onLeft(Timber::e) + } + .retryWhen { cause, _ -> + Timber.e("Retry updating quotes: $cause") + + emit(cause.left()) + + delay(timeMillis = 2000) + true + } + } + + @VisibleForTesting(VisibleForTesting.NONE) + fun getMultiQuoteUpdatesFlow(): EitherFlow = getMultiQuoteUpdates() + + @VisibleForTesting(VisibleForTesting.NONE) + fun getUpdaterJobHolder(): JobHolder = updaterHolder +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt index 2ae06d88e6..9fbbe97e50 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt @@ -47,6 +47,8 @@ internal class DefaultQuotesStoreV2( override fun get(): Flow> = runtimeStore.get() + override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() + override suspend fun refresh(currenciesIds: Set) { updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt index b530a6559c..ed31f3abf9 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt @@ -11,6 +11,9 @@ internal interface QuotesStoreV2 { /** Get flow of quotes */ fun get(): Flow> + /** Get all quotes synchronously or null */ + suspend fun getAllSyncOrNull(): Set? + /** Refresh status of [currenciesIds] */ suspend fun refresh(currenciesIds: Set) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt index b35a5c6f06..3e85896a19 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -32,11 +33,12 @@ internal class DefaultMultiQuoteFetcherTest { tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, quotesStore = quotesStore, + dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch quotes successfully`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -64,9 +66,78 @@ internal class DefaultMultiQuoteFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + @Test + fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) + + val actual = fetcher(params) + + coVerify(inverse = true) { + quotesStore.refresh(currenciesIds = any()) + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(values = any()) + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest { + val appCurrencyId = "usd" + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + + val coinIds = "BTC,ETH" + coEvery { + tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeActual(values = successResponse.quotes) + } + + coVerify(inverse = true) { + appCurrencyResponseStore.getSyncOrNull() + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest { + val appCurrencyId = "" + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(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 { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -97,7 +168,7 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes failure because app currency not found`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt new file mode 100644 index 0000000000..f14962f52b --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt @@ -0,0 +1,169 @@ +package com.tangem.data.quotes.multi + +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteUpdaterTest { + + private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() + private val quotesStore: QuotesStoreV2 = mockk() + private val multiQuoteFetcher: MultiQuoteFetcher = mockk() + + private val multiQuoteUpdater = DefaultMultiQuoteUpdater( + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStore = quotesStore, + multiQuoteFetcher = multiQuoteFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test that initial app currency is skipped`() = runTest { + val appCurrencyFlow = flowOf(null, usdAppCurrency) + + every { appCurrencyResponseStore.get() } returns appCurrencyFlow + coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() + + val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteFetcher(params) } returns Unit.right() + + val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val appCurrencyFlow = flowOf(usdAppCurrency, usdAppCurrency) + + every { appCurrencyResponseStore.get() } returns appCurrencyFlow + coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() + + val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteFetcher(params) } returns Unit.right() + + val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `test that flow is filtered the null`() = runTest { + val appCurrencyFlow = flowOf(null, null) + + every { appCurrencyResponseStore.get() } returns appCurrencyFlow + + val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values = getEmittedValues(actual) + Truth.assertThat(values.size).isEqualTo(0) + + coVerify(inverse = true) { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(any()) + } + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + + val innerFlow = MutableStateFlow(value = false) + val appCurrencyFlow = flow { + if (innerFlow.value) { + emitAll(flowOf(null, usdAppCurrency)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { appCurrencyResponseStore.get() } returns appCurrencyFlow + coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() + + val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteFetcher(params) } returns Unit.right() + + val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() + + coVerify(exactly = 1) { appCurrencyResponseStore.get() } + + val values1 = getEmittedValues(actual) + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first().isLeft()).isTrue() + Truth.assertThat(values1.first().leftOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(values1.first().leftOrNull()).hasMessageThat().isEqualTo(exception.message) + + coVerify(inverse = true) { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(any()) + } + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + Truth.assertThat(values2).isEqualTo(listOf(Unit.right())) + + coVerifyOrder { + quotesStore.getAllSyncOrNull() + multiQuoteFetcher(params) + } + } + + @Test + fun `subscribe and unsubscribe successfully`() { + every { appCurrencyResponseStore.get() } returns emptyFlow() + + val updaterJobHolder = multiQuoteUpdater.getUpdaterJobHolder() + Truth.assertThat(updaterJobHolder.isEmpty()).isTrue() + + multiQuoteUpdater.subscribe() + + Truth.assertThat(updaterJobHolder.isEmpty()).isFalse() + + multiQuoteUpdater.unsubscribe() + + Truth.assertThat(updaterJobHolder.isEmpty()).isTrue() + } + + private companion object { + val usdAppCurrency = CurrenciesResponse.Currency( + id = "USD".lowercase(), + code = "USD", + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ) + } +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt index bbfd1053a6..d5a517f5e2 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt @@ -60,4 +60,33 @@ internal class QuotesStoreGetMethodTest { Truth.assertThat(values.size).isEqualTo(1) Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain()))) } + + @Test + fun `test getAllSyncOrNull if runtime store is empty`() = runTest { + val actual = store.getAllSyncOrNull() + + Truth.assertThat(actual).isEqualTo(null) + } + + @Test + fun `test getAllSyncOrNull if runtime store contains empty set`() = runTest { + runtimeStore.store(value = emptySet()) + + val actual = store.getAllSyncOrNull() + + Truth.assertThat(actual).isEqualTo(emptySet()) + } + + @Test + fun `test getAllSyncOrNull if runtime store is not empty`() = runTest { + val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + + runtimeStore.store(value = setOf(btcQuote.toDomain(), ethQuote.toDomain())) + + val actual = store.getAllSyncOrNull() + + val expected = setOf(btcQuote.toDomain(), ethQuote.toDomain()) + Truth.assertThat(actual).isEqualTo(expected) + } } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt index 4520ad1fd6..4319ae8399 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt @@ -10,5 +10,14 @@ import com.tangem.domain.tokens.model.CryptoCurrency */ interface MultiQuoteFetcher : FlowFetcher { - data class Params(val currenciesIds: Set) + /** + * Params + * + * @property currenciesIds identifiers of currencies + * @property appCurrencyId app currency id, if null then selected app currency will be used + */ + data class Params( + val currenciesIds: Set, + val appCurrencyId: String?, + ) } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt new file mode 100644 index 0000000000..ee490bac35 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteUpdater.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.quotes.multi + +/** + * Updater of quotes + * +[REDACTED_AUTHOR] + */ +interface MultiQuoteUpdater { + + /** Subscribe */ + fun subscribe() + + /** Unsubscribe */ + fun unsubscribe() +} \ No newline at end of file