Updated on 2026-08-14
This commit is contained in:
parent
eef5e02b3e
commit
8d5c9e0e58
11 changed files with 448 additions and 31 deletions
|
|
@ -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<QuotesResponse.Quote>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
): DataStore<Map<String, QuotesResponse.Quote>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQuotesStore(persistenceStore: DataStore<Map<String, QuotesResponse.Quote>>): QuotesStore {
|
||||
return DefaultQuotesStore(
|
||||
persistenceStore = persistenceStore,
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Throwable, Unit> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, Unit> {
|
||||
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<Throwable, Unit> = getMultiQuoteUpdates()
|
||||
|
||||
@VisibleForTesting(VisibleForTesting.NONE)
|
||||
fun getUpdaterJobHolder(): JobHolder = updaterHolder
|
||||
}
|
||||
|
|
@ -47,6 +47,8 @@ internal class DefaultQuotesStoreV2(
|
|||
|
||||
override fun get(): Flow<Set<Quote>> = runtimeStore.get()
|
||||
|
||||
override suspend fun getAllSyncOrNull(): Set<Quote>? = runtimeStore.getSyncOrNull()
|
||||
|
||||
override suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>) {
|
||||
updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ internal interface QuotesStoreV2 {
|
|||
/** Get flow of quotes */
|
||||
fun get(): Flow<Set<Quote>>
|
||||
|
||||
/** Get all quotes synchronously or null */
|
||||
suspend fun getAllSyncOrNull(): Set<Quote>?
|
||||
|
||||
/** Refresh status of [currenciesIds] */
|
||||
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Quote>())
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,5 +10,14 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
*/
|
||||
interface MultiQuoteFetcher : FlowFetcher<MultiQuoteFetcher.Params> {
|
||||
|
||||
data class Params(val currenciesIds: Set<CryptoCurrency.RawID>)
|
||||
/**
|
||||
* 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<CryptoCurrency.RawID>,
|
||||
val appCurrencyId: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.quotes.multi
|
||||
|
||||
/**
|
||||
* Updater of quotes
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MultiQuoteUpdater {
|
||||
|
||||
/** Subscribe */
|
||||
fun subscribe()
|
||||
|
||||
/** Unsubscribe */
|
||||
fun unsubscribe()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue