Updated on 2026-08-14
This commit is contained in:
parent
7832fae9f3
commit
e2026ec410
5 changed files with 527 additions and 30 deletions
|
|
@ -121,8 +121,8 @@ internal class DefaultNFTRuntimeStore(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val assetsCount = when (assets) {
|
val assetsCount = when {
|
||||||
is NFTCollection.Assets.Value -> assets.items.size
|
assets is NFTCollection.Assets.Value && assets.items.isNotEmpty() -> assets.items.size
|
||||||
else -> data.count
|
else -> data.count
|
||||||
}
|
}
|
||||||
data.copy(
|
data.copy(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.datasource.local.nft
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.core.DataStoreFactory
|
import androidx.datastore.core.DataStoreFactory
|
||||||
|
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||||
import androidx.datastore.dataStoreFile
|
import androidx.datastore.dataStoreFile
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.tangem.blockchain.nft.models.NFTCollection
|
import com.tangem.blockchain.nft.models.NFTCollection
|
||||||
|
|
@ -58,6 +59,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
||||||
types = types,
|
types = types,
|
||||||
defaultValue = defaultValue,
|
defaultValue = defaultValue,
|
||||||
),
|
),
|
||||||
|
corruptionHandler = ReplaceFileCorruptionHandler { defaultValue },
|
||||||
produceFile = { context.dataStoreFile(fileName = fileName) },
|
produceFile = { context.dataStoreFile(fileName = fileName) },
|
||||||
scope = appScope,
|
scope = appScope,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package com.tangem.datasource.local.nft
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.nft.models.NFTAsset
|
||||||
|
import com.tangem.domain.nft.models.NFTCollection
|
||||||
|
import com.tangem.domain.nft.models.NFTCollections
|
||||||
|
import com.tangem.domain.nft.models.NFTSalePrice
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
class DefaultNFTRuntimeStoreTest {
|
||||||
|
|
||||||
|
private val network = createNetwork()
|
||||||
|
|
||||||
|
private val store = DefaultNFTRuntimeStore(
|
||||||
|
network = network,
|
||||||
|
collectionsRuntimeStore = RuntimeSharedStore(),
|
||||||
|
pricesRuntimeStore = RuntimeSharedStore(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN collection with empty loaded assets WHEN getCollections THEN collection is kept`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val collection = createCollection(
|
||||||
|
count = 1,
|
||||||
|
assets = NFTCollection.Assets.Value(items = emptyList(), source = StatusSource.ACTUAL),
|
||||||
|
)
|
||||||
|
store.initialize(collections = createCollections(collection), prices = emptyMap())
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val content = store.getCollections().first().content
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java)
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
val actual = content.collections.orEmpty().single()
|
||||||
|
assertThat(actual.count).isEqualTo(1)
|
||||||
|
assertThat(actual.assets).isInstanceOf(NFTCollection.Assets.Value::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN collection with loaded assets WHEN getCollections THEN count recalculated from assets`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val collection = createCollection(
|
||||||
|
count = 5,
|
||||||
|
assets = NFTCollection.Assets.Value(items = listOf(createAsset()), source = StatusSource.ACTUAL),
|
||||||
|
)
|
||||||
|
store.initialize(collections = createCollections(collection), prices = emptyMap())
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val content = store.getCollections().first().content
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
assertThat(content.collections.orEmpty().single().count).isEqualTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN collection with zero count and not loaded assets WHEN getCollections THEN collection filtered out`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange
|
||||||
|
val collection = createCollection(count = 0, assets = NFTCollection.Assets.Empty)
|
||||||
|
store.initialize(collections = createCollections(collection), prices = emptyMap())
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val content = store.getCollections().first().content
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
assertThat(content.collections.orEmpty()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCollections(vararg collections: NFTCollection) = NFTCollections(
|
||||||
|
network = network,
|
||||||
|
content = NFTCollections.Content.Collections(
|
||||||
|
collections = collections.toList(),
|
||||||
|
source = StatusSource.ACTUAL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createCollection(count: Int, assets: NFTCollection.Assets) = NFTCollection(
|
||||||
|
id = NFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS),
|
||||||
|
network = network,
|
||||||
|
name = "Test collection",
|
||||||
|
description = null,
|
||||||
|
logoUrl = null,
|
||||||
|
count = count,
|
||||||
|
assets = assets,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createAsset(): NFTAsset {
|
||||||
|
val assetId = NFTAsset.Identifier.EVM(
|
||||||
|
tokenAddress = TOKEN_ADDRESS,
|
||||||
|
tokenId = BigInteger.ONE,
|
||||||
|
contractType = NFTAsset.Identifier.EVM.ContractType.ERC721,
|
||||||
|
)
|
||||||
|
return NFTAsset(
|
||||||
|
id = assetId,
|
||||||
|
collectionId = NFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS),
|
||||||
|
network = network,
|
||||||
|
contractType = "ERC721",
|
||||||
|
owner = null,
|
||||||
|
name = "Test asset",
|
||||||
|
description = null,
|
||||||
|
amount = null,
|
||||||
|
decimals = 0,
|
||||||
|
salePrice = NFTSalePrice.Empty(assetId),
|
||||||
|
rarity = null,
|
||||||
|
media = null,
|
||||||
|
traits = emptyList(),
|
||||||
|
source = StatusSource.ACTUAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNetwork() = Network(
|
||||||
|
id = Network.ID(rawId = Network.RawID("ethereum"), derivationPath = Network.DerivationPath.None),
|
||||||
|
name = "Ethereum",
|
||||||
|
currencySymbol = "ETH",
|
||||||
|
derivationPath = Network.DerivationPath.None,
|
||||||
|
isTestnet = false,
|
||||||
|
standardType = Network.StandardType.ERC20,
|
||||||
|
hasFiatFeeRate = true,
|
||||||
|
canHandleTokens = true,
|
||||||
|
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||||
|
nameResolvingType = Network.NameResolvingType.NONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TOKEN_ADDRESS = "0x0000000000000000000000000000000000000001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.datasource.local.nft.NFTPersistenceStore
|
||||||
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
|
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
|
||||||
import com.tangem.datasource.local.nft.NFTRuntimeStore
|
import com.tangem.datasource.local.nft.NFTRuntimeStore
|
||||||
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
||||||
|
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
|
||||||
import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter
|
import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter
|
||||||
import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter
|
import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter
|
||||||
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
|
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
|
||||||
|
|
@ -181,29 +182,27 @@ internal class DefaultNFTRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNFTPersistenceStore(userWalletId, network)
|
saveAssetsInRuntime(
|
||||||
.getCollectionsSync()
|
userWalletId = userWalletId,
|
||||||
?.map { collection ->
|
network = network,
|
||||||
if (collection.identifier == sdkCollectionId) {
|
collectionId = collectionId,
|
||||||
collection.copy(assets = assets)
|
assets = assets,
|
||||||
} else {
|
)
|
||||||
collection
|
|
||||||
}
|
// local cache failures must not affect the runtime state which is already up to date
|
||||||
}
|
runSuspendCatching {
|
||||||
?.let { collections ->
|
updateAssetsInPersistence(
|
||||||
saveCollectionsInRuntime(
|
userWalletId = userWalletId,
|
||||||
userWalletId = userWalletId,
|
network = network,
|
||||||
network = network,
|
sdkCollectionId = sdkCollectionId,
|
||||||
collections = collections,
|
assets = assets,
|
||||||
)
|
)
|
||||||
saveCollectionsInPersistence(
|
}.onFailure { error ->
|
||||||
userWalletId = userWalletId,
|
TangemLogger.e("Failed to persist NFT assets for $network", error)
|
||||||
network = network,
|
}
|
||||||
collections = collections,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.onLeft { throwable ->
|
}.onLeft { throwable ->
|
||||||
if (throwable !is UnsupportedOperationException) {
|
if (throwable !is UnsupportedOperationException) {
|
||||||
|
TangemLogger.e("Failed to refresh NFT assets for $network", throwable)
|
||||||
saveFailedStateInRuntime(
|
saveFailedStateInRuntime(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = network,
|
network = network,
|
||||||
|
|
@ -269,18 +268,29 @@ internal class DefaultNFTRepository @Inject constructor(
|
||||||
expireCollections(userWalletId, network)
|
expireCollections(userWalletId, network)
|
||||||
|
|
||||||
val collections = walletManagersFacade.getNFTCollections(userWalletId, network)
|
val collections = walletManagersFacade.getNFTCollections(userWalletId, network)
|
||||||
val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network)
|
|
||||||
|
// local cache failures must not affect successfully fetched collections
|
||||||
|
val mergedCollections = runSuspendCatching {
|
||||||
|
collections.mergeWithStoredAssets(userWalletId, network)
|
||||||
|
}.getOrElse { error ->
|
||||||
|
TangemLogger.e("Failed to merge NFT collections with stored assets for $network", error)
|
||||||
|
collections
|
||||||
|
}
|
||||||
|
|
||||||
saveCollectionsInRuntime(
|
saveCollectionsInRuntime(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = network,
|
network = network,
|
||||||
collections = mergedCollections,
|
collections = mergedCollections,
|
||||||
)
|
)
|
||||||
saveCollectionsInPersistence(
|
runSuspendCatching {
|
||||||
userWalletId = userWalletId,
|
saveCollectionsInPersistence(
|
||||||
network = network,
|
userWalletId = userWalletId,
|
||||||
collections = mergedCollections,
|
network = network,
|
||||||
)
|
collections = mergedCollections,
|
||||||
|
)
|
||||||
|
}.onFailure { error ->
|
||||||
|
TangemLogger.e("Failed to persist NFT collections for $network", error)
|
||||||
|
}
|
||||||
|
|
||||||
if (refreshAssets) {
|
if (refreshAssets) {
|
||||||
mergedCollections.forEach { collection ->
|
mergedCollections.forEach { collection ->
|
||||||
|
|
@ -292,6 +302,7 @@ internal class DefaultNFTRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onLeft { throwable ->
|
}.onLeft { throwable ->
|
||||||
|
TangemLogger.e("Failed to refresh NFT collections for $network", throwable)
|
||||||
saveFailedStateInRuntime(
|
saveFailedStateInRuntime(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = network,
|
network = network,
|
||||||
|
|
@ -391,12 +402,72 @@ internal class DefaultNFTRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun saveAssetsInRuntime(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
collectionId: NFTCollection.Identifier,
|
||||||
|
assets: List<SdkNFTAsset>,
|
||||||
|
) {
|
||||||
|
val store = getNFTRuntimeStore(userWalletId, network)
|
||||||
|
val storedCollections = store.getCollectionsSync()
|
||||||
|
val content = storedCollections.content as? NFTCollections.Content.Collections ?: return
|
||||||
|
|
||||||
|
val convertedAssets = assets
|
||||||
|
.map { asset -> NFTSdkAssetConverter.convert(network to asset) }
|
||||||
|
.filter { it.id !is NFTAsset.Identifier.Unknown }
|
||||||
|
|
||||||
|
val updatedCollections = content.collections
|
||||||
|
?.map { collection ->
|
||||||
|
if (collection.id == collectionId) {
|
||||||
|
collection.copy(
|
||||||
|
assets = NFTCollection.Assets.Value(
|
||||||
|
items = convertedAssets,
|
||||||
|
source = StatusSource.ACTUAL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.saveCollections(
|
||||||
|
storedCollections.copy(content = content.copy(collections = updatedCollections)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateAssetsInPersistence(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
sdkCollectionId: SdkNFTCollection.Identifier,
|
||||||
|
assets: List<SdkNFTAsset>,
|
||||||
|
) {
|
||||||
|
val storedCollections = getNFTPersistenceStore(userWalletId, network).getCollectionsSync() ?: return
|
||||||
|
val updatedCollections = storedCollections.map { collection ->
|
||||||
|
if (collection.identifier == sdkCollectionId) {
|
||||||
|
collection.copy(assets = assets)
|
||||||
|
} else {
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveCollectionsInPersistence(userWalletId, network, updatedCollections)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveCollectionsInPersistence(
|
private suspend fun saveCollectionsInPersistence(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
network: Network,
|
network: Network,
|
||||||
collections: List<SdkNFTCollection>,
|
collections: List<SdkNFTCollection>,
|
||||||
) {
|
) {
|
||||||
getNFTPersistenceStore(userWalletId, network).saveCollections(collections)
|
val serializableCollections = collections
|
||||||
|
.filter { it.identifier !is SdkNFTCollection.Identifier.Unknown }
|
||||||
|
.map { collection ->
|
||||||
|
collection.copy(
|
||||||
|
assets = collection.assets.filter { asset ->
|
||||||
|
asset.identifier !is SdkNFTAsset.Identifier.Unknown &&
|
||||||
|
asset.collectionIdentifier !is SdkNFTCollection.Identifier.Unknown
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getNFTPersistenceStore(userWalletId, network).saveCollections(serializableCollections)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveSalePriceInRuntime(userWalletId: UserWalletId, network: Network, salePrice: NFTSalePrice) {
|
private suspend fun saveSalePriceInRuntime(userWalletId: UserWalletId, network: Network, salePrice: NFTSalePrice) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
package com.tangem.data.nft
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||||
|
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||||
|
import com.tangem.datasource.local.nft.NFTPersistenceStore
|
||||||
|
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
|
||||||
|
import com.tangem.datasource.local.nft.NFTRuntimeStore
|
||||||
|
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
|
||||||
|
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter
|
||||||
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.nft.models.NFTAsset
|
||||||
|
import com.tangem.domain.nft.models.NFTCollection
|
||||||
|
import com.tangem.domain.nft.models.NFTCollections
|
||||||
|
import com.tangem.domain.nft.models.NFTSalePrice
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import java.io.IOException
|
||||||
|
import java.math.BigInteger
|
||||||
|
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||||
|
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
class DefaultNFTRepositoryTest {
|
||||||
|
|
||||||
|
private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk()
|
||||||
|
private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk()
|
||||||
|
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||||
|
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||||
|
private val context: Context = mockk()
|
||||||
|
|
||||||
|
private val userWalletId = UserWalletId("011")
|
||||||
|
private val userWallet = mockk<UserWallet.Hot> {
|
||||||
|
every { walletId } returns userWalletId
|
||||||
|
}
|
||||||
|
|
||||||
|
private val network: Network = MockCryptoCurrencyFactory().ethereum.network
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun resetMocks() {
|
||||||
|
clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory, walletManagersFacade, userWalletsListRepository)
|
||||||
|
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet))
|
||||||
|
every { context.resources } returns mockk()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRepository() = DefaultNFTRepository(
|
||||||
|
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
|
||||||
|
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
networkFactory = mockk(),
|
||||||
|
excludedBlockchains = ExcludedBlockchains(),
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN collections fetched WHEN persistence write fails THEN runtime keeps actual data`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val runtimeStore = FakeNFTRuntimeStore(network)
|
||||||
|
val persistenceStore = mockk<NFTPersistenceStore> {
|
||||||
|
coEvery { getCollectionsSync() } returns null
|
||||||
|
coEvery { getSalePricesSync() } returns null
|
||||||
|
coEvery { saveCollections(any()) } throws IOException("Failed to write to disk")
|
||||||
|
}
|
||||||
|
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore
|
||||||
|
every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore
|
||||||
|
coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } returns listOf(createSdkCollection())
|
||||||
|
|
||||||
|
// Act
|
||||||
|
createRepository().refreshCollections(userWalletId, listOf(network))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val content = runtimeStore.getCollectionsSync().content
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java)
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
assertThat(content.source).isEqualTo(StatusSource.ACTUAL)
|
||||||
|
assertThat(content.collections).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN runtime has collection missing in persistence WHEN refreshAssets THEN assets saved to runtime`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange
|
||||||
|
val sdkCollection = createSdkCollection()
|
||||||
|
val collectionId = NFTSdkCollectionIdentifierConverter.convert(sdkCollection.identifier)
|
||||||
|
val runtimeStore = FakeNFTRuntimeStore(network)
|
||||||
|
val persistenceStore = mockk<NFTPersistenceStore> {
|
||||||
|
coEvery { getCollectionsSync() } returns null
|
||||||
|
coEvery { getSalePricesSync() } returns null
|
||||||
|
coEvery { saveCollections(any()) } returns Unit
|
||||||
|
}
|
||||||
|
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore
|
||||||
|
every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore
|
||||||
|
coEvery {
|
||||||
|
walletManagersFacade.getNFTCollections(userWalletId, network)
|
||||||
|
} returns listOf(sdkCollection)
|
||||||
|
coEvery {
|
||||||
|
walletManagersFacade.getNFTAssets(userWalletId, network, sdkCollection.identifier)
|
||||||
|
} returns listOf(createSdkAsset())
|
||||||
|
coEvery { walletManagersFacade.getNFTSalePrice(userWalletId, network, any(), any()) } returns null
|
||||||
|
|
||||||
|
val repository = createRepository()
|
||||||
|
// seed runtime store with the fetched collection, persistence stays empty
|
||||||
|
repository.refreshCollections(userWalletId, listOf(network))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
repository.refreshAssets(userWalletId, network, collectionId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val content = runtimeStore.getCollectionsSync().content
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java)
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
val assets = content.collections.orEmpty().single().assets
|
||||||
|
assertThat(assets).isInstanceOf(NFTCollection.Assets.Value::class.java)
|
||||||
|
assets as NFTCollection.Assets.Value
|
||||||
|
assertThat(assets.items).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN fetch returns no assets WHEN refreshAssets THEN empty loaded value saved to runtime`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val sdkCollection = createSdkCollection()
|
||||||
|
val collectionId = NFTSdkCollectionIdentifierConverter.convert(sdkCollection.identifier)
|
||||||
|
val runtimeStore = FakeNFTRuntimeStore(network)
|
||||||
|
val persistenceStore = mockk<NFTPersistenceStore> {
|
||||||
|
coEvery { getCollectionsSync() } returns null
|
||||||
|
coEvery { getSalePricesSync() } returns null
|
||||||
|
coEvery { saveCollections(any()) } returns Unit
|
||||||
|
}
|
||||||
|
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore
|
||||||
|
every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore
|
||||||
|
coEvery {
|
||||||
|
walletManagersFacade.getNFTCollections(userWalletId, network)
|
||||||
|
} returns listOf(sdkCollection)
|
||||||
|
coEvery {
|
||||||
|
walletManagersFacade.getNFTAssets(userWalletId, network, sdkCollection.identifier)
|
||||||
|
} returns emptyList()
|
||||||
|
|
||||||
|
val repository = createRepository()
|
||||||
|
repository.refreshCollections(userWalletId, listOf(network))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
repository.refreshAssets(userWalletId, network, collectionId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val content = runtimeStore.getCollectionsSync().content
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java)
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
val assets = content.collections.orEmpty().single().assets
|
||||||
|
assertThat(assets).isInstanceOf(NFTCollection.Assets.Value::class.java)
|
||||||
|
assets as NFTCollection.Assets.Value
|
||||||
|
assertThat(assets.items).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no cached collections WHEN fetch fails THEN error state saved to runtime`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val runtimeStore = FakeNFTRuntimeStore(network)
|
||||||
|
val persistenceStore = mockk<NFTPersistenceStore> {
|
||||||
|
coEvery { getCollectionsSync() } returns null
|
||||||
|
coEvery { getSalePricesSync() } returns null
|
||||||
|
}
|
||||||
|
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore
|
||||||
|
every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore
|
||||||
|
coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } throws IOException("HTTP 500")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
createRepository().refreshCollections(userWalletId, listOf(network))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val content = runtimeStore.getCollectionsSync().content
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Error::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN cached collections WHEN fetch fails THEN cache marked as only cache`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val runtimeStore = FakeNFTRuntimeStore(network)
|
||||||
|
val persistenceStore = mockk<NFTPersistenceStore> {
|
||||||
|
coEvery { getCollectionsSync() } returns listOf(createSdkCollection())
|
||||||
|
coEvery { getSalePricesSync() } returns null
|
||||||
|
}
|
||||||
|
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceStore
|
||||||
|
every { nftRuntimeStoreFactory.provide(network) } returns runtimeStore
|
||||||
|
coEvery { walletManagersFacade.getNFTCollections(userWalletId, network) } throws IOException("HTTP 500")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
createRepository().refreshCollections(userWalletId, listOf(network))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val content = runtimeStore.getCollectionsSync().content
|
||||||
|
assertThat(content).isInstanceOf(NFTCollections.Content.Collections::class.java)
|
||||||
|
content as NFTCollections.Content.Collections
|
||||||
|
assertThat(content.source).isEqualTo(StatusSource.ONLY_CACHE)
|
||||||
|
assertThat(content.collections).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSdkCollection(assets: List<SdkNFTAsset> = emptyList()) = SdkNFTCollection(
|
||||||
|
identifier = SdkNFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS),
|
||||||
|
blockchainId = Blockchain.Ethereum.id,
|
||||||
|
name = "Test collection",
|
||||||
|
description = null,
|
||||||
|
logoUrl = null,
|
||||||
|
count = 1,
|
||||||
|
assets = assets,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createSdkAsset() = SdkNFTAsset(
|
||||||
|
identifier = SdkNFTAsset.Identifier.EVM(
|
||||||
|
tokenId = BigInteger.ONE,
|
||||||
|
tokenAddress = TOKEN_ADDRESS,
|
||||||
|
contractType = SdkNFTAsset.Identifier.EVM.ContractType.ERC721,
|
||||||
|
),
|
||||||
|
collectionIdentifier = SdkNFTCollection.Identifier.EVM(tokenAddress = TOKEN_ADDRESS),
|
||||||
|
blockchainId = Blockchain.Ethereum.id,
|
||||||
|
contractType = "ERC721",
|
||||||
|
owner = null,
|
||||||
|
name = "Test asset",
|
||||||
|
description = null,
|
||||||
|
amount = BigInteger.ONE,
|
||||||
|
decimals = 0,
|
||||||
|
salePrice = null,
|
||||||
|
rarity = null,
|
||||||
|
media = null,
|
||||||
|
traits = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class FakeNFTRuntimeStore(private val network: Network) : NFTRuntimeStore {
|
||||||
|
|
||||||
|
private var collections: NFTCollections = NFTCollections.empty(network)
|
||||||
|
private var prices: Map<NFTAsset.Identifier, NFTSalePrice> = emptyMap()
|
||||||
|
|
||||||
|
override suspend fun initialize(collections: NFTCollections, prices: Map<NFTAsset.Identifier, NFTSalePrice>) {
|
||||||
|
this.collections = collections
|
||||||
|
this.prices = prices
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCollections(): Flow<NFTCollections> = flowOf(collections)
|
||||||
|
|
||||||
|
override suspend fun getCollectionsSync(): NFTCollections = collections
|
||||||
|
|
||||||
|
override fun getAsset(
|
||||||
|
collectionId: NFTCollection.Identifier,
|
||||||
|
assetId: NFTAsset.Identifier,
|
||||||
|
): Flow<NFTAsset?> = flowOf(null)
|
||||||
|
|
||||||
|
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTSalePrice> =
|
||||||
|
flowOf(prices[assetId] ?: NFTSalePrice.Empty(assetId))
|
||||||
|
|
||||||
|
override suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice =
|
||||||
|
prices[assetId] ?: NFTSalePrice.Empty(assetId)
|
||||||
|
|
||||||
|
override suspend fun saveCollections(collections: NFTCollections) {
|
||||||
|
this.collections = collections
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSalePrice(salePrice: NFTSalePrice) {
|
||||||
|
prices = prices + (salePrice.assetId to salePrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
collections = NFTCollections.empty(network)
|
||||||
|
prices = emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TOKEN_ADDRESS = "0x0000000000000000000000000000000000000001"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue