Updated on 2026-08-14
This commit is contained in:
parent
14c20117eb
commit
a398620b0d
14 changed files with 477 additions and 4 deletions
|
|
@ -9,6 +9,9 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.core.datasource)
|
||||||
|
implementation(projects.core.utils)
|
||||||
|
|
||||||
implementation(projects.data.common)
|
implementation(projects.data.common)
|
||||||
|
|
||||||
implementation(projects.domain.legacy)
|
implementation(projects.domain.legacy)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.common.test.data.quote
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
object MockQuoteResponseFactory {
|
||||||
|
|
||||||
|
fun createSinglePrice(value: BigDecimal): QuotesResponse.Quote {
|
||||||
|
return QuotesResponse.Quote(
|
||||||
|
price = value,
|
||||||
|
priceChange24h = value,
|
||||||
|
priceChange1w = value,
|
||||||
|
priceChange30d = value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.common.test.data.quote
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import com.tangem.datasource.local.quote.converter.QuoteConverter
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
|
||||||
|
fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote {
|
||||||
|
return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): Quote {
|
||||||
|
return QuoteConverter(source = source).convert(value = mapOf(this).entries.first())
|
||||||
|
}
|
||||||
|
|
@ -10,13 +10,24 @@ import com.tangem.utils.extensions.orZero
|
||||||
/**
|
/**
|
||||||
* Converter from [QuotesResponse.Quote] to [Quote.Value]
|
* Converter from [QuotesResponse.Quote] to [Quote.Value]
|
||||||
*
|
*
|
||||||
* @property isCached flag that determines whether the quote is a cache
|
* @property source status source
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
internal class QuoteConverter(private val isCached: Boolean) :
|
class QuoteConverter(
|
||||||
|
private val source: StatusSource,
|
||||||
|
) :
|
||||||
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
|
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondary constructor
|
||||||
|
*
|
||||||
|
* @param isCached flag that determines whether the quote is a cache
|
||||||
|
*/
|
||||||
|
constructor(isCached: Boolean) : this(
|
||||||
|
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
||||||
|
)
|
||||||
|
|
||||||
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
|
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
|
||||||
val (currencyId, quote) = value
|
val (currencyId, quote) = value
|
||||||
|
|
||||||
|
|
@ -24,7 +35,7 @@ internal class QuoteConverter(private val isCached: Boolean) :
|
||||||
rawCurrencyId = CryptoCurrency.RawID(currencyId),
|
rawCurrencyId = CryptoCurrency.RawID(currencyId),
|
||||||
fiatRate = quote.price.orZero(),
|
fiatRate = quote.price.orZero(),
|
||||||
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
|
||||||
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
source = source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ internal class NetworksStatusesStoreInitializationTest {
|
||||||
|
|
||||||
DefaultNetworksStatusesStoreV2(
|
DefaultNetworksStatusesStoreV2(
|
||||||
runtimeStore = runtimeStore,
|
runtimeStore = runtimeStore,
|
||||||
persistenceDataStore = persistenceStore, // local mock
|
persistenceDataStore = persistenceStore,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
1
data/quotes/.gitignore
vendored
Normal file
1
data/quotes/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
30
data/quotes/build.gradle.kts
Normal file
30
data/quotes/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.android.library)
|
||||||
|
alias(deps.plugins.kotlin.android)
|
||||||
|
alias(deps.plugins.kotlin.kapt)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.tangem.data.quotes"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.core.datasource)
|
||||||
|
implementation(projects.core.utils)
|
||||||
|
|
||||||
|
implementation(projects.domain.models)
|
||||||
|
implementation(projects.domain.tokens.models)
|
||||||
|
implementation(projects.domain.wallets.models)
|
||||||
|
|
||||||
|
implementation(deps.androidx.datastore)
|
||||||
|
|
||||||
|
implementation(deps.hilt.android)
|
||||||
|
kapt(deps.hilt.kapt)
|
||||||
|
|
||||||
|
testImplementation(deps.test.coroutine)
|
||||||
|
testImplementation(deps.test.junit)
|
||||||
|
testImplementation(deps.test.mockk)
|
||||||
|
testImplementation(deps.test.truth)
|
||||||
|
testImplementation(projects.common.test)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.tangem.data.quotes.store
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.datasource.local.quote.converter.QuoteConverter
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.extensions.addOrReplace
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [QuotesStoreV2]
|
||||||
|
*
|
||||||
|
* @property runtimeStore runtime store
|
||||||
|
* @property persistenceDataStore persistence store
|
||||||
|
* @param dispatchers dispatchers
|
||||||
|
*/
|
||||||
|
internal class DefaultQuotesStoreV2(
|
||||||
|
private val runtimeStore: RuntimeSharedStore<Set<Quote>>,
|
||||||
|
private val persistenceDataStore: DataStore<CurrencyIdWithQuote>,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : QuotesStoreV2 {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
val cachedStatuses = persistenceDataStore.data.firstOrNull()
|
||||||
|
|
||||||
|
if (cachedStatuses.isNullOrEmpty()) return@launch
|
||||||
|
|
||||||
|
runtimeStore.store(
|
||||||
|
value = QuoteConverter(isCached = true).convertSet(input = cachedStatuses.entries),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(): Flow<Set<Quote>> = runtimeStore.get()
|
||||||
|
|
||||||
|
override suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>) {
|
||||||
|
updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun storeActual(values: Map<String, QuotesResponse.Quote>) {
|
||||||
|
coroutineScope {
|
||||||
|
launch {
|
||||||
|
val quotes = QuoteConverter(isCached = false).convertSet(input = values.entries)
|
||||||
|
storeInRuntimeStore(values = quotes)
|
||||||
|
}
|
||||||
|
launch { storeInPersistenceStore(values = values) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun storeError(currenciesIds: Set<CryptoCurrency.RawID>) {
|
||||||
|
updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.ONLY_CACHE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateStatusSourceInRuntime(currenciesIds: Set<CryptoCurrency.RawID>, source: StatusSource) {
|
||||||
|
runtimeStore.update(default = emptySet()) { stored ->
|
||||||
|
val updatedQuotes = currenciesIds.mapTo(hashSetOf()) { id ->
|
||||||
|
val quote = stored.firstOrNull { it.rawCurrencyId == id } ?: Quote.Empty(id)
|
||||||
|
|
||||||
|
quote.copySealed(source = source)
|
||||||
|
}
|
||||||
|
|
||||||
|
stored.addOrReplace(items = updatedQuotes) { old, new -> old.rawCurrencyId == new.rawCurrencyId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInRuntimeStore(values: Set<Quote>) {
|
||||||
|
runtimeStore.update(default = emptySet()) { saved ->
|
||||||
|
saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInPersistenceStore(values: Map<String, QuotesResponse.Quote>) {
|
||||||
|
persistenceDataStore.updateData { storedQuotes -> storedQuotes + values }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.tangem.data.quotes.store
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||||
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/** Store of [Quote]'es set */
|
||||||
|
internal interface QuotesStoreV2 {
|
||||||
|
|
||||||
|
/** Get flow of quotes */
|
||||||
|
fun get(): Flow<Set<Quote>>
|
||||||
|
|
||||||
|
/** Refresh status of [currenciesIds] */
|
||||||
|
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)
|
||||||
|
|
||||||
|
/** Store actual map of currency ids and quotes [values] */
|
||||||
|
suspend fun storeActual(values: Map<String, QuotesResponse.Quote>)
|
||||||
|
|
||||||
|
/** Store error for [currenciesIds] */
|
||||||
|
suspend fun storeError(currenciesIds: Set<CryptoCurrency.RawID>)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.tangem.data.quotes.store
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||||
|
import com.tangem.common.test.data.quote.toDomain
|
||||||
|
import com.tangem.common.test.datastore.MockStateDataStore
|
||||||
|
import com.tangem.common.test.utils.getEmittedValues
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class QuotesStoreGetMethodTest {
|
||||||
|
|
||||||
|
private val runtimeStore = RuntimeSharedStore<Set<Quote>>()
|
||||||
|
private val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||||
|
|
||||||
|
private val store = DefaultQuotesStoreV2(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test get if runtime store is empty`() = runTest {
|
||||||
|
val actual = store.get()
|
||||||
|
|
||||||
|
val values = backgroundScope.getEmittedValues(testScheduler, actual)
|
||||||
|
|
||||||
|
Truth.assertThat(values).isEqualTo(emptyList<Set<Quote>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test get if runtime store contains empty set`() = runTest {
|
||||||
|
runtimeStore.store(value = emptySet())
|
||||||
|
|
||||||
|
val actual = store.get()
|
||||||
|
|
||||||
|
val values = backgroundScope.getEmittedValues(testScheduler, actual)
|
||||||
|
|
||||||
|
Truth.assertThat(values).isEqualTo(listOf(emptySet<Quote>()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test get 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.get()
|
||||||
|
|
||||||
|
val values = backgroundScope.getEmittedValues(testScheduler, actual)
|
||||||
|
|
||||||
|
Truth.assertThat(values.size).isEqualTo(1)
|
||||||
|
Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.tangem.data.quotes.store
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||||
|
import com.tangem.common.test.data.quote.toDomain
|
||||||
|
import com.tangem.common.test.datastore.MockStateDataStore
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class QuotesStoreInitializationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test initialization if cache store is empty`() = runTest {
|
||||||
|
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
|
||||||
|
val persistenceStore: DataStore<CurrencyIdWithQuote> = mockk()
|
||||||
|
|
||||||
|
every { persistenceStore.data } returns emptyFlow()
|
||||||
|
|
||||||
|
DefaultQuotesStoreV2(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test initialization if cache store contains empty map`() = runTest {
|
||||||
|
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
|
||||||
|
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||||
|
|
||||||
|
DefaultQuotesStoreV2(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test initialization if cache store is not empty`() = runTest {
|
||||||
|
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
|
||||||
|
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||||
|
|
||||||
|
val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
|
||||||
|
val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||||
|
|
||||||
|
persistenceStore.updateData {
|
||||||
|
it.toMutableMap().apply {
|
||||||
|
this += btcQuote
|
||||||
|
this += ethQuote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultQuotesStoreV2(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val expected = setOf(
|
||||||
|
btcQuote.toDomain(source = StatusSource.CACHE),
|
||||||
|
ethQuote.toDomain(source = StatusSource.CACHE),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
package com.tangem.data.quotes.store
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
|
||||||
|
import com.tangem.common.test.data.quote.toDomain
|
||||||
|
import com.tangem.common.test.datastore.MockStateDataStore
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.model.Quote
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class QuotesStoreUpdateMethodsTest {
|
||||||
|
|
||||||
|
private val runtimeStore = RuntimeSharedStore<Set<Quote>>()
|
||||||
|
private val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||||
|
|
||||||
|
private val store = DefaultQuotesStoreV2(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `refresh if runtime store is empty`() = runTest {
|
||||||
|
val currenciesIds = setOf(
|
||||||
|
CryptoCurrency.RawID(value = "BTC"),
|
||||||
|
CryptoCurrency.RawID(value = "ETH"),
|
||||||
|
)
|
||||||
|
|
||||||
|
store.refresh(currenciesIds = currenciesIds)
|
||||||
|
|
||||||
|
val runtimeExpected = currenciesIds.map(Quote::Empty).toSet()
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||||
|
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `refresh if runtime store contains quote with this id`() = runTest {
|
||||||
|
val quote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE)
|
||||||
|
.toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL)
|
||||||
|
|
||||||
|
runtimeStore.store(value = setOf(quote))
|
||||||
|
|
||||||
|
store.refresh(currenciesIds = setOf(quote.rawCurrencyId))
|
||||||
|
|
||||||
|
val runtimeExpected = setOf(quote.copySealed(source = StatusSource.CACHE))
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||||
|
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `store actual if runtime and cache stores contain quotes with this id`() = runTest {
|
||||||
|
val prevStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE)
|
||||||
|
|
||||||
|
runtimeStore.store(
|
||||||
|
value = setOf(
|
||||||
|
prevStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
persistenceStore.updateData {
|
||||||
|
it.toMutableMap().apply {
|
||||||
|
put("BTC", prevStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN)
|
||||||
|
|
||||||
|
store.storeActual(values = mapOf("BTC" to newStatus))
|
||||||
|
|
||||||
|
val runtimeExpected = setOf(
|
||||||
|
newStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL),
|
||||||
|
)
|
||||||
|
val persistenceExpected = mapOf("BTC" to newStatus)
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||||
|
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `store error if runtime store is empty`() = runTest {
|
||||||
|
val currenciesIds = setOf(
|
||||||
|
CryptoCurrency.RawID(value = "BTC"),
|
||||||
|
CryptoCurrency.RawID(value = "ETH"),
|
||||||
|
)
|
||||||
|
|
||||||
|
store.storeError(currenciesIds = currenciesIds)
|
||||||
|
|
||||||
|
val runtimeExpected = currenciesIds.map(Quote::Empty).toSet()
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||||
|
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `store error if runtime store contains status with this network`() = runTest {
|
||||||
|
val status = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE)
|
||||||
|
|
||||||
|
runtimeStore.store(
|
||||||
|
value = setOf(
|
||||||
|
status.toDomain(rawCurrencyId = "BTC", source = StatusSource.CACHE),
|
||||||
|
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
store.storeError(
|
||||||
|
currenciesIds = setOf(
|
||||||
|
CryptoCurrency.RawID(value = "BTC"),
|
||||||
|
CryptoCurrency.RawID(value = "ETH"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val runtimeExpected = setOf(
|
||||||
|
status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE),
|
||||||
|
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
|
||||||
|
)
|
||||||
|
|
||||||
|
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||||
|
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,13 @@ sealed interface Quote {
|
||||||
|
|
||||||
val rawCurrencyId: CryptoCurrency.RawID
|
val rawCurrencyId: CryptoCurrency.RawID
|
||||||
|
|
||||||
|
fun copySealed(source: StatusSource): Quote {
|
||||||
|
return when (this) {
|
||||||
|
is Empty -> this
|
||||||
|
is Value -> copy(source = source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents unknown financial information for a specific cryptocurrency.
|
* Represents unknown financial information for a specific cryptocurrency.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -314,4 +314,5 @@ include(":data:manage-tokens")
|
||||||
include(":data:networks")
|
include(":data:networks")
|
||||||
include(":data:nft")
|
include(":data:nft")
|
||||||
include(":data:onramp")
|
include(":data:onramp")
|
||||||
|
include(":data:quotes")
|
||||||
// endregion Data modules
|
// endregion Data modules
|
||||||
Loading…
Add table
Add a link
Reference in a new issue