diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 9bb7985754..7f412108e3 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -80,7 +80,7 @@ internal class DefaultNetworksStatusesStore( ifNotFound: (Network.ID) -> SimpleNetworkStatus?, ) { if (networks.isEmpty()) { - Timber.d("Nothing to update: networks is empty") + Timber.d("Nothing to update: networks are empty") return } diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 368d9b3bef..41c386a5c4 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.data.quotes" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) @@ -30,7 +34,8 @@ dependencies { kapt(deps.hilt.kapt) testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt index ed18015ad2..af55873878 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt @@ -1,6 +1,6 @@ package com.tangem.data.quotes -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.model.QuoteStatus @@ -14,7 +14,7 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultQuotesRepositoryV2 @Inject constructor( - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, ) : QuotesRepositoryV2 { override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index 58803a1a56..fb43a38727 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -5,8 +5,8 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi import com.tangem.data.quotes.DefaultQuotesRepositoryV2 -import com.tangem.data.quotes.store.DefaultQuotesStoreV2 -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.DefaultQuotesStatusesStore +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore @@ -33,8 +33,8 @@ internal object QuotesDataModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, - ): QuotesStoreV2 { - return DefaultQuotesStoreV2( + ): QuotesStatusesStore { + return DefaultQuotesStatusesStore( runtimeStore = RuntimeSharedStore(), persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( 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 cfa6b766f0..830d3afef4 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 @@ -2,7 +2,9 @@ package com.tangem.data.quotes.multi import arrow.core.Either import com.tangem.data.common.api.safeApiCallWithTimeout -import com.tangem.data.quotes.store.QuotesStoreV2 +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.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore @@ -29,7 +31,7 @@ import javax.inject.Singleton internal class DefaultMultiQuoteFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteFetcher { @@ -41,7 +43,7 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( return@catchOn } - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( @@ -67,11 +69,11 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( filteredIds = replacementIdsResult.idsFiltered, ) - quotesStore.storeActual(values = updatedResponse.quotes) + quotesStore.store(values = updatedResponse.quotes) } .onLeft { Timber.e(it) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { 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 index 8f0ae6297f..f3f01a2e1d 100644 --- 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 @@ -2,7 +2,7 @@ package com.tangem.data.quotes.multi import androidx.annotation.VisibleForTesting import arrow.core.left -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.quotes.multi.MultiQuoteFetcher @@ -33,7 +33,7 @@ import javax.inject.Singleton @Singleton internal class DefaultMultiQuoteUpdater @Inject constructor( private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val multiQuoteFetcher: MultiQuoteFetcher, dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteUpdater { diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt index 796d0dceac..490804a7ea 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -1,6 +1,6 @@ package com.tangem.data.quotes.single -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.quotes.single.SingleQuoteProducer import com.tangem.domain.tokens.model.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.mapNotNull */ internal class DefaultSingleQuoteProducer @AssistedInject constructor( @Assisted val params: SingleQuoteProducer.Params, - private val quotesStore: QuotesStoreV2, + private val quotesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : SingleQuoteProducer { 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/DefaultQuotesStatusesStore.kt similarity index 68% rename from data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 2158d4d6a8..6cdb7bd157 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/DefaultQuotesStatusesStore.kt @@ -15,21 +15,22 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import timber.log.Timber internal typealias CurrencyIdWithQuote = Map /** - * Default implementation of [QuotesStoreV2] + * Default implementation of [QuotesStatusesStore] * * @property runtimeStore runtime store * @property persistenceDataStore persistence store * @param dispatchers dispatchers */ -internal class DefaultQuotesStoreV2( +internal class DefaultQuotesStatusesStore( private val runtimeStore: RuntimeSharedStore>, private val persistenceDataStore: DataStore, dispatchers: CoroutineDispatcherProvider, -) : QuotesStoreV2 { +) : QuotesStatusesStore { private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) @@ -49,33 +50,28 @@ internal class DefaultQuotesStoreV2( override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() - override suspend fun refresh(currenciesIds: Set) { - updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) - } - - override suspend fun storeActual(values: Map) { - coroutineScope { - launch { - val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) - storeInRuntimeStore(values = quotes) - } - launch { storeInPersistenceStore(values = values) } - } - } - - override suspend fun storeError(currenciesIds: Set) { - updateStatusSourceInRuntime( - currenciesIds = currenciesIds, - ifNotFound = ::QuoteStatus, - source = StatusSource.ONLY_CACHE, + override suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, + ) { + updateStatusSource( + currenciesIds = setOf(currencyId), + source = source, + ifNotFound = ifNotFound, ) } - private suspend fun updateStatusSourceInRuntime( + override suspend fun updateStatusSource( currenciesIds: Set, - ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, ) { + if (currenciesIds.isEmpty()) { + Timber.d("Nothing to update: currencies ids are empty") + return + } + runtimeStore.update(default = emptySet()) { stored -> val updatedQuotes = currenciesIds.mapNotNullTo(hashSetOf()) { id -> val quote = stored.firstOrNull { it.rawCurrencyId == id } @@ -89,13 +85,24 @@ internal class DefaultQuotesStoreV2( } } - private suspend fun storeInRuntimeStore(values: Set) { - runtimeStore.update(default = emptySet()) { saved -> - saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + override suspend fun store(values: CurrencyIdWithQuote) { + if (values.isEmpty()) return + + coroutineScope { + launch { storeInRuntime(values = values) } + launch { storeInPersistence(values = values) } } } - private suspend fun storeInPersistenceStore(values: Map) { + private suspend fun storeInRuntime(values: CurrencyIdWithQuote) { + val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries) + + runtimeStore.update(default = emptySet()) { saved -> + saved.addOrReplace(items = quotes) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + } + } + + private suspend fun storeInPersistence(values: CurrencyIdWithQuote) { persistenceDataStore.updateData { storedQuotes -> storedQuotes + values } } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt new file mode 100644 index 0000000000..140b84c495 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt @@ -0,0 +1,46 @@ +package com.tangem.data.quotes.store + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +import kotlinx.coroutines.flow.Flow + +/** Store of [QuoteStatus]'es set */ +internal interface QuotesStatusesStore { + + /** Get flow of quotes */ + fun get(): Flow> + + /** Get all quotes synchronously or null */ + suspend fun getAllSyncOrNull(): Set? + + /** + * Update [source] of [QuoteStatus] by [currencyId]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Update [source] of [QuoteStatus]es by [currenciesIds]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currenciesIds: Set, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Store quotes statuses + * + * @param values map of currency ids and quotes + * + * See complex methods in `QuotesStatusesStoreExt`. + */ + suspend fun store(values: Map) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt new file mode 100644 index 0000000000..849519a417 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.store + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus + +/** Set [StatusSource] of quotes statuses as [StatusSource.CACHE] for [currenciesIds] */ +internal suspend fun QuotesStatusesStore.setSourceAsCache(currenciesIds: Set) { + updateStatusSource(currenciesIds = currenciesIds, source = StatusSource.CACHE) +} + +/** + * Set [StatusSource] of quotes statuses as [StatusSource.ONLY_CACHE] for [currenciesIds]. + * If the stored status is not found, store a default [QuoteStatus.Empty] status. + */ +internal suspend fun QuotesStatusesStore.setSourceAsOnlyCache(currenciesIds: Set) { + updateStatusSource( + currenciesIds = currenciesIds, + source = StatusSource.ONLY_CACHE, + ifNotFound = ::QuoteStatus, + ) +} \ No newline at end of file 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 deleted file mode 100644 index b29bb240af..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.data.quotes.store - -import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus -import kotlinx.coroutines.flow.Flow - -/** Store of [QuoteStatus]'es set */ -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) - - /** Store actual map of currency ids and quotes [values] */ - suspend fun storeActual(values: Map) - - /** Store error for [currenciesIds] */ - suspend fun storeError(currenciesIds: Set) -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index d49e65f597..4969adec58 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -2,7 +2,9 @@ package com.tangem.data.quotes.multi import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.store.QuotesStoreV2 +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 @@ -27,7 +29,7 @@ internal class DefaultMultiQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) private val fetcher = DefaultMultiQuoteFetcher( tangemTechApi = tangemTechApi, @@ -50,17 +52,17 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -73,11 +75,11 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerify(inverse = true) { - quotesStore.refresh(currenciesIds = any()) + quotesStore.setSourceAsCache(currenciesIds = any()) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) - quotesStore.storeError(currenciesIds = any()) + quotesStore.store(values = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -96,16 +98,16 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -119,14 +121,14 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -150,17 +152,17 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -175,14 +177,14 @@ internal class DefaultMultiQuoteStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt index d51f1c0460..0e7eeae889 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt @@ -3,7 +3,7 @@ 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.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.quotes.multi.MultiQuoteFetcher @@ -19,7 +19,7 @@ import org.junit.Test internal class DefaultMultiQuoteStatusUpdaterTest { private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() - private val quotesStore: QuotesStoreV2 = mockk() + private val quotesStore: QuotesStatusesStore = mockk() private val multiQuoteFetcher: MultiQuoteFetcher = mockk() private val multiQuoteUpdater = DefaultMultiQuoteUpdater( diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index 83108ee088..131263866d 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -3,7 +3,9 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher -import com.tangem.data.quotes.store.QuotesStoreV2 +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 @@ -18,14 +20,14 @@ import io.mockk.coVerify import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.Test import java.math.BigDecimal internal class DefaultSingleQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) private val multiFetcher = DefaultMultiQuoteFetcher( tangemTechApi = tangemTechApi, @@ -50,14 +52,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -76,9 +78,9 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } Truth.assertThat(actual.isRight()).isTrue() @@ -92,8 +94,8 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } Truth.assertThat(actual.isLeft()).isTrue() @@ -117,14 +119,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -139,14 +141,14 @@ internal class DefaultSingleQuoteStatusFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index 774a1b9d9f..6bca20b07d 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -2,7 +2,7 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.single.SingleQuoteProducer @@ -25,7 +25,7 @@ internal class DefaultSingleQuoteStatusProducerTest { rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), ) - private val quotesStore = mockk() + private val quotesStore = mockk() private val producer = DefaultSingleQuoteProducer( params = params, diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt new file mode 100644 index 0000000000..6713800ad0 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt @@ -0,0 +1,133 @@ +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.ProvideTestModels +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreExtTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val btcQuote = btcQuoteDM.toDomain() + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = emptySet(), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsOnlyCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsOnlyCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsOnlyCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(QuoteStatus(btcQuote.rawCurrencyId)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + ) + } + + data class SetSourceTestModel( + val initialRuntime: Set?, + val currenciesIds: Set, + val expected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt new file mode 100644 index 0000000000..6fb34da4e7 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt @@ -0,0 +1,565 @@ +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.common.test.utils.ProvideTestModels +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.tokens.model.QuoteStatus +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.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + // region Data models + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val ethQuoteDM = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + // endregion + + // region Domain models + private val btcQuote = btcQuoteDM.toDomain() + private val ethQuote = ethQuoteDM.toDomain() + private val adaEmptyQuote = QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ADA")) + // endregion + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @Test + fun `initialization if cache store is empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store contains empty map`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store is not empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + persistenceStore.updateData { + it.toMutableMap().apply { + this += btcQuoteDM + this += ethQuoteDM + } + } + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = setOf( + btcQuoteDM.toDomain(source = StatusSource.CACHE), + ethQuoteDM.toDomain(source = StatusSource.CACHE), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Get { + + @ParameterizedTest + @ProvideTestModels + fun get(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = getEmittedValues(flow = store.get()) + + // Assert + val expected = listOfNotNull(model.expected) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetAllSyncOrNull { + + @ParameterizedTest + @ProvideTestModels + fun getAllSyncOrNull(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + data class GetTestModel(val initialRuntime: Set?, val expected: Set?) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SingleUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Single) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currencyId = model.currencyId, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote, adaEmptyQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + ethQuote, + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + ethQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, ethQuote), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote), + currencyId = adaEmptyQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MultiUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Multi) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currenciesIds = model.currenciesIds, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = null, + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, adaEmptyQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + sealed interface UpdateStatusSourceModel { + + val initialRuntime: Set? + val source: StatusSource + val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? + val expected: Set? + + data class Single( + override val initialRuntime: Set?, + val currencyId: CryptoCurrency.RawID, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + + data class Multi( + override val initialRuntime: Set?, + val currenciesIds: Set, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Store { + + @ParameterizedTest + @ProvideTestModels + fun store(model: StoreTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + if (model.initialPersistence != null) { + persistenceStore.updateData { model.initialPersistence } + } + + // Act + store.store(values = model.values) + + val runtimeActual = runtimeStore.getSyncOrNull() + val persistenceActual = getEmittedValues(persistenceStore.data) + + // Assert + val runtimeExpected = model.runtimeExpected + val persistenceExpected = listOf(model.persistenceExpected) + + Truth.assertThat(runtimeActual).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceActual).isEqualTo(persistenceExpected) + } + + private fun provideTestModels() = listOf( + // region stores are null + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region persistence store is null + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = null, + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = setOf(ethQuote), + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region runtime store is null + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(ethQuoteDM), + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + + // region stores contain data + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = mapOf(ethQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + ) + } + + data class StoreTestModel( + val initialRuntime: Set?, + val initialPersistence: CurrencyIdWithQuote?, + val values: CurrencyIdWithQuote, + val persistenceExpected: CurrencyIdWithQuote?, + val runtimeExpected: Set?, + ) +} \ 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 deleted file mode 100644 index b957e0624e..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -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.QuoteStatus -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>() - private val persistenceStore = MockStateDataStore(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 = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(emptyList>()) - } - - @Test - fun `test get if runtime store contains empty set`() = runTest { - runtimeStore.store(value = emptySet()) - - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(listOf(emptySet())) - } - - @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 = getEmittedValues(flow = actual) - - 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/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt deleted file mode 100644 index e60dd2bd09..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -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.QuoteStatus -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>() - val persistenceStore: DataStore = 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>() - val persistenceStore = MockStateDataStore(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>() - val persistenceStore = MockStateDataStore(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) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt deleted file mode 100644 index d3224d5c48..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -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.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.QuoteStatus -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>() - private val persistenceStore = MockStateDataStore(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) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @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.copy(value = quote.value.copySealed(source = StatusSource.CACHE)), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @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 { QuoteStatus(rawCurrencyId = it, value = QuoteStatus.Empty) } - .toSet() - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @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), - QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), - ), - ) - - store.storeError( - currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ), - ) - - val runtimeExpected = setOf( - status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } -} \ No newline at end of file