Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:39:30 +03:00
commit 92849d43c2
1407 changed files with 64092 additions and 11990 deletions

View file

@ -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())
}
@ -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"

View file

@ -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)
@ -47,7 +46,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 +58,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 +94,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 +111,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 +155,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 +167,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 +190,7 @@ internal class ExpressHistoryConverterTest {
)
// WHEN
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
val entity = item.toEntity()
// THEN
Truth.assertThat(entity.failReason).isNull()
@ -269,8 +279,4 @@ internal class ExpressHistoryConverterTest {
paymentMethod = "card",
countryCode = "US",
)
private companion object {
const val OWNER_ADDRESS = "0xowner"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}