Updated on 2026-08-14
This commit is contained in:
commit
e0033cef82
1726 changed files with 83681 additions and 14949 deletions
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.local.promotion
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
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
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultPromotionsSupplierTest {
|
||||
|
||||
private val tangemApi: TangemTechApi = mockk()
|
||||
|
||||
private fun newSupplier() = DefaultPromotionsSupplier(
|
||||
tangemApi = tangemApi,
|
||||
store = RuntimeSharedStore(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
private val response = PromotionsResponse(promotions = emptyList())
|
||||
private val response2 = PromotionsResponse(
|
||||
promotions = listOf(
|
||||
PromotionsResponse.PromotionDto(name = "dummy", all = null),
|
||||
),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(tangemApi)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty cache WHEN getPromotions THEN fetches and returns response`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) { tangemApi.getPromotions(userWalletId.stringValue, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached value and no refresh WHEN getPromotions THEN returns cache without api`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
clearMocks(tangemApi)
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 0) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached value WHEN getPromotions forceRefresh THEN hits api again and rebinds value`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returnsMany listOf(
|
||||
ApiResponse.Success(response),
|
||||
ApiResponse.Success(response2),
|
||||
)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response2)
|
||||
coVerify(exactly = 2) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and cache present WHEN getPromotions forceRefresh THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("boom")
|
||||
|
||||
// Act
|
||||
val error = runCatching { supplier.getPromotions(userWalletId, forceRefresh = true) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and empty cache WHEN getPromotions THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("boom")
|
||||
val supplier = newSupplier()
|
||||
|
||||
// Act
|
||||
val error = runCatching { supplier.getPromotions(userWalletId) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appInfoProvider.osVersion } returns "Android 16"
|
||||
every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag()
|
||||
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
every { appInfoProvider.deviceScale } returns DEVICE_SCALE
|
||||
|
||||
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
|
||||
}
|
||||
|
|
@ -170,7 +171,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
expected = ApiEnvironmentConfig(
|
||||
environment = environment,
|
||||
baseUrl = when (environment) {
|
||||
ApiEnvironment.PROD -> "https://authentication.tangem.org/"
|
||||
ApiEnvironment.PROD -> "https://api.tangem.org/"
|
||||
else -> "[REDACTED_ENV_URL]"
|
||||
},
|
||||
headers = emptyMap(),
|
||||
|
|
@ -298,6 +299,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -313,6 +315,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -457,6 +460,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private companion object {
|
||||
|
||||
const val VERSION_NAME = "debug"
|
||||
const val DEVICE_SCALE = 3f
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.datasource.api.marketing
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
|
||||
import com.tangem.datasource.api.marketing.models.MarketingCampaignsResponse
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class MarketingCampaignsResponseTest {
|
||||
|
||||
private val moshi = Moshi.Builder().add(BigDecimalAdapter()).build()
|
||||
private val adapter = moshi.adapter(MarketingCampaignsResponse::class.java)
|
||||
|
||||
@Test
|
||||
fun `GIVEN swap response json WHEN parsed THEN fields mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"swap","priority":1,"minAmount":50,"maxAmount":300,
|
||||
"providerIds":["provider1"],
|
||||
"banner":{"uiType":"linked_to_provider","text":"Cashback 4 U","icon":"https://x/star.webp",
|
||||
"bgColor":"#FF0011","deeplink":"https://tangem.com","dismissible":true}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)!!
|
||||
|
||||
// Assert
|
||||
val campaign = result.campaigns.single()
|
||||
assertThat(campaign.id).isEqualTo(12)
|
||||
assertThat(campaign.type).isEqualTo("swap")
|
||||
assertThat(campaign.minAmount).isEqualTo(BigDecimal(50))
|
||||
assertThat(campaign.providerIds).containsExactly("provider1")
|
||||
assertThat(campaign.banner.uiType).isEqualTo("linked_to_provider")
|
||||
assertThat(campaign.banner.isDismissible).isTrue()
|
||||
assertThat(campaign.tokens).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token_details response WHEN parsed THEN network targets mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"token_details","priority":1,
|
||||
"tokens":[{"networkId":"ethereum","contractAddress":"0xA0b8"}],
|
||||
"banner":{"uiType":"standalone","dismissible":false}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val campaign = adapter.fromJson(json)!!.campaigns.single()
|
||||
|
||||
// Assert
|
||||
val token = campaign.tokens!!.single()
|
||||
assertThat(token.networkId).isEqualTo("ethereum")
|
||||
assertThat(token.contractAddress).isEqualTo("0xA0b8")
|
||||
assertThat(token.id).isNull()
|
||||
assertThat(campaign.minAmount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN markets response WHEN parsed THEN coingecko ids mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"markets_token","priority":1,
|
||||
"tokens":[{"id":"1696501400"},{"id":"3296501412"}],
|
||||
"banner":{"uiType":"standalone","dismissible":true}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val tokens = adapter.fromJson(json)!!.campaigns.single().tokens!!
|
||||
|
||||
// Assert
|
||||
assertThat(tokens.map { it.id }).containsExactly("1696501400", "3296501412")
|
||||
assertThat(tokens.all { it.networkId == null }).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,10 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.txId).isEqualTo(item.txId)
|
||||
Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS)
|
||||
Truth.assertThat(entity.providerId).isEqualTo(item.providerId)
|
||||
Truth.assertThat(entity.fromAddress).isEqualTo(item.fromAddress)
|
||||
Truth.assertThat(entity.payinAddress).isEqualTo(item.payinAddress)
|
||||
|
|
@ -35,8 +34,7 @@ internal class ExpressHistoryConverterTest {
|
|||
Truth.assertThat(entity.refundNetwork).isEqualTo(item.refundNetwork)
|
||||
Truth.assertThat(entity.refundContractAddress).isEqualTo(item.refundContractAddress)
|
||||
Truth.assertThat(entity.createdAt).isEqualTo(item.createdAt)
|
||||
// todo txHistory uncomment
|
||||
// Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt)
|
||||
Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt)
|
||||
Truth.assertThat(entity.payTill).isEqualTo(item.payTill)
|
||||
Truth.assertThat(entity.averageDuration).isEqualTo(item.averageDuration)
|
||||
}
|
||||
|
|
@ -47,7 +45,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem(status = "finished")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("finished")
|
||||
|
|
@ -59,7 +57,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress)
|
||||
|
|
@ -95,7 +93,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.payinExtraId).isNull()
|
||||
|
|
@ -112,17 +110,28 @@ internal class ExpressHistoryConverterTest {
|
|||
Truth.assertThat(entity.to.actualAmount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN exchange item with null fromAddress WHEN toEntity THEN returns null`() {
|
||||
// GIVEN
|
||||
val item = createExchangeItem().copy(fromAddress = null)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onramp item WHEN toEntity THEN all transaction fields are mapped`() {
|
||||
// GIVEN
|
||||
val item = createOnrampItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.txId).isEqualTo(item.txId)
|
||||
Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS)
|
||||
Truth.assertThat(entity.providerId).isEqualTo(item.providerId)
|
||||
Truth.assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress)
|
||||
Truth.assertThat(entity.failReason).isEqualTo(item.failReason)
|
||||
|
|
@ -145,7 +154,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem(status = "waiting-for-payment")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("waiting-for-payment")
|
||||
|
|
@ -157,7 +166,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress)
|
||||
|
|
@ -180,7 +189,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.failReason).isNull()
|
||||
|
|
@ -223,8 +232,7 @@ internal class ExpressHistoryConverterTest {
|
|||
refundNetwork = refundNetwork,
|
||||
refundContractAddress = refundContractAddress,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
// todo txHistory uncomment
|
||||
// updatedAt = "2026-06-01T00:05:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
payTill = payTill,
|
||||
averageDuration = averageDuration,
|
||||
fromContractAddress = "0xfromContract",
|
||||
|
|
@ -256,8 +264,7 @@ internal class ExpressHistoryConverterTest {
|
|||
externalTxUrl = externalTxUrl,
|
||||
payoutHash = payoutHash,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
// todo txHistory uncomment
|
||||
// updatedAt = "2026-06-01T00:05:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
fromCurrencyCode = "USD",
|
||||
fromAmount = "100.0",
|
||||
fromPrecision = 2,
|
||||
|
|
@ -269,8 +276,4 @@ internal class ExpressHistoryConverterTest {
|
|||
paymentMethod = "card",
|
||||
countryCode = "US",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val OWNER_ADDRESS = "0xowner"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
* Tests for [DefaultTangemPayReissueCardStore], focused on the reissue order-id lifecycle.
|
||||
*
|
||||
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] so the preferences
|
||||
* round-trip (store / read / remove) is exercised end-to-end. The remove path backs the [REDACTED_TASK_KEY] fix:
|
||||
* a terminal reissue order must be forgotten so the payment-account refresh stops re-polling
|
||||
* `GET /order/{id}` for it.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayReissueCardStoreTest {
|
||||
|
||||
private val dataStore = MockStateDataStore(default = emptyPreferences())
|
||||
private val prefs = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = dataStore,
|
||||
)
|
||||
private val feeStore: RuntimeDataStore<TangemPayReissueCardFee> = mockk(relaxed = true)
|
||||
|
||||
private val store = DefaultTangemPayReissueCardStore(feeStore = feeStore, prefs = prefs)
|
||||
|
||||
@BeforeEach
|
||||
fun resetStore() {
|
||||
runBlocking { dataStore.updateData { emptyPreferences() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN getOrderId THEN returns stored id`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
val result = store.getOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(ORDER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN removeReissueOrderId THEN order id is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN clearing one card WHEN another card has an order THEN the other is untouched`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
store.storeReissueOrderId(OTHER_CARD_ID, OTHER_ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
assertThat(store.getOrderId(OTHER_CARD_ID)).isEqualTo(OTHER_ORDER_ID)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CARD_ID = "card-1"
|
||||
const val OTHER_CARD_ID = "card-2"
|
||||
const val ORDER_ID = "reissue-order-1"
|
||||
const val OTHER_ORDER_ID = "reissue-order-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayTxHistoryItemsStoreTest {
|
||||
|
||||
private lateinit var store: DefaultTangemPayTxHistoryItemsStore
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
store = DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = MockStateDataStore(default = emptyMap()),
|
||||
toDMConverter = TangemPayTxHistoryItemToDMConverter(),
|
||||
toDomainConverter = TangemPayTxHistoryItemToDomainConverter(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN getSyncOrNull THEN returns null`() = runTest {
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored page WHEN getSyncOrNull with same key and cursor THEN returns page`() = runTest {
|
||||
// Arrange
|
||||
val page = listOf(payment("1"), payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page)
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN page stored under one cursor WHEN getSyncOrNull with another cursor THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("1")))
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_2)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two cursors stored for one wallet WHEN getSyncOrNull THEN both pages are kept`() = runTest {
|
||||
// Arrange
|
||||
val page1 = listOf(payment("1"))
|
||||
val page2 = listOf(payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page1)
|
||||
store.store(key = WALLET_A, cursor = CURSOR_2, value = page2)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isEqualTo(page1)
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_2)).isEqualTo(page2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove one wallet THEN only that wallet is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(WALLET_A)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isEqualTo(listOf(payment("b")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove both in one call THEN both are cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(listOf(WALLET_A, WALLET_B))
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isNull()
|
||||
}
|
||||
|
||||
private fun payment(id: String) = TangemPayTxHistoryItem.Payment(
|
||||
id = id,
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("1.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val WALLET_A = "wallet-a"
|
||||
const val WALLET_B = "wallet-b"
|
||||
const val CURSOR_1 = "cursor-1"
|
||||
const val CURSOR_2 = "cursor-2"
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryStoreSerializationTest {
|
||||
|
||||
private val json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" }
|
||||
|
||||
private val serializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN all tx history item types WHEN serialized and deserialized THEN value is preserved`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf(
|
||||
"initial_cursor_key" to listOf(spend(), payment(), fee(), collateral()),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Collateral with type field WHEN serialized and deserialized THEN discriminator does not clash`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tx history item WHEN serialized THEN uses stable SerialName under non-clashing discriminator`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val encoded = json.encodeToString(serializer, original)
|
||||
|
||||
// Assert
|
||||
assertThat(encoded).contains("\"__type\":\"collateral\"")
|
||||
}
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItemDM.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItemDM.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItemDM.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItemDM.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItemDM.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItemDM.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryItemDMConverterTest {
|
||||
|
||||
private val toDMConverter = TangemPayTxHistoryItemToDMConverter()
|
||||
private val toDomainConverter = TangemPayTxHistoryItemToDomainConverter()
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN every item subtype WHEN converted to DM and back THEN all fields preserved`(
|
||||
item: TangemPayTxHistoryItem,
|
||||
) {
|
||||
assertRoundTrip(item)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Status::class)
|
||||
fun `GIVEN every Spend status WHEN converted to DM and back THEN status preserved`(
|
||||
status: TangemPayTxHistoryItem.Status,
|
||||
) {
|
||||
assertRoundTrip(spend().copy(status = status))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Type::class)
|
||||
fun `GIVEN every Collateral type WHEN converted to DM and back THEN type preserved`(
|
||||
type: TangemPayTxHistoryItem.Type,
|
||||
) {
|
||||
assertRoundTrip(collateral().copy(type = type))
|
||||
}
|
||||
|
||||
private fun assertRoundTrip(item: TangemPayTxHistoryItem) {
|
||||
// Act
|
||||
val restored = toDomainConverter.convert(toDMConverter.convert(item))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(item)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(spend(), payment(), fee(), collateral())
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItem.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItem.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItem.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItem.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItem.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItem.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue