Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-08 18:10:31 +04:00
parent c43f3b8059
commit 3cd6e47a5d
10 changed files with 254 additions and 225 deletions

View file

@ -73,7 +73,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
if (BuildConfig.TESTER_MENU_ENABLED) {
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.map {
when (it[ApiConfig.ID.Attestation.name]) {
when (it[ApiConfig.ID.TangemTech.name]) {
ApiEnvironment.DEV,
ApiEnvironment.STAGE,
-> false
@ -189,7 +189,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
config = config.apply {
isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled
val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Attestation)
val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech)
isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD
},
)

View file

@ -92,4 +92,5 @@ dependencies {
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
}

View file

@ -25,7 +25,6 @@ sealed class ApiConfig {
TangemTech,
StakeKit,
TangemPay,
Attestation,
BlockAid,
}
@ -35,7 +34,6 @@ sealed class ApiConfig {
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is TangemPay -> ID.TangemPay
is Attestation -> ID.Attestation
is BlockAid -> ID.BlockAid
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.api.common.config
/**
* Attestation [ApiConfig] that used in CardSDK
*
[REDACTED_AUTHOR]
*/
internal class Attestation : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createProdEnvironment(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/",
)
}

View file

@ -16,21 +16,28 @@ internal class TangemTech(
override val environmentConfigs = listOf(
createDevEnvironment(),
createStageEnvironment(),
createProdEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",
headers = createHeaders(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",
headers = createHeaders(),
)
private fun createHeaders() = buildMap {
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)

View file

@ -60,8 +60,4 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet
fun provideAttestationConfig(): ApiConfig = Attestation()
}

View file

@ -15,6 +15,7 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
private val environmentConfig = EnvironmentConfig(
express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"),
devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"),
blockAidApiKey = BLOCK_AID_API_KEY,
)
override suspend fun initialize() = environmentConfig
@ -24,5 +25,6 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
companion object {
const val EXPRESS_API_KEY = "express_api_key"
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
const val BLOCK_AID_API_KEY = "block_aid_api_key"
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
import android.os.Build
import com.google.common.truth.Truth
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
@ -10,46 +11,47 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import java.util.Locale
import java.util.TimeZone
private val configManager = MockEnvironmentConfigStorage()
private val appVersionProvider = mockk<AppVersionProvider>()
private val expressAuthProvider = mockk<ExpressAuthProvider>()
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
private val appAuthProvider = mockk<AuthProvider>()
private val appInfoProvider = mockk<AppInfoProvider>()
// Don't forget to add new config !!!
private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider, appInfoProvider),
TangemTech(appVersionProvider, appAuthProvider, appInfoProvider),
StakeKit(stakeKitAuthProvider),
TangemPay(appVersionProvider),
)
/**
[REDACTED_AUTHOR]
*/
@RunWith(Parameterized::class)
internal class ProdApiConfigsManagerTest(private val model: Model) {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ProdApiConfigsManagerTest {
private val manager = ProdApiConfigsManager(API_CONFIGS)
private val environmentConfigStorage = MockEnvironmentConfigStorage()
private val appVersionProvider = mockk<AppVersionProvider>()
private val expressAuthProvider = mockk<ExpressAuthProvider>()
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
private val appAuthProvider = mockk<AuthProvider>()
private val appInfoProvider = mockk<AppInfoProvider>()
@Before
private val manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
@BeforeEach
fun setup() {
clearMocks(
appVersionProvider,
expressAuthProvider,
stakeKitAuthProvider,
appAuthProvider,
appInfoProvider,
)
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
@ -58,8 +60,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
every { appInfoProvider.osVersion } returns "Android 16"
}
@Test
fun test_getEnvironmentConfig() {
@ParameterizedTest
@ProvideTestModels
fun getEnvironmentConfig(model: TestModel) {
val actual = manager.getEnvironmentConfig(id = model.id)
Truth.assertThat(actual.environment).isEqualTo(model.expected.environment)
@ -69,160 +72,175 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
.isEqualTo(model.expected.headers.mapValues { runBlocking { it.value() } })
}
data class Model(val id: ApiConfig.ID, val expected: ApiEnvironmentConfig)
private fun createApiConfigs(): ApiConfigs {
return ApiConfig.ID.entries.mapTo(destination = hashSetOf()) {
when (it) {
ApiConfig.ID.Express -> {
Express(
environmentConfigStorage = environmentConfigStorage,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
ApiConfig.ID.TangemTech -> {
TangemTech(
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,
)
}
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
}
}
}
internal companion object {
private fun provideTestModels() = ApiConfig.ID.entries.map {
when (it) {
ApiConfig.ID.Express -> createExpressModel()
ApiConfig.ID.TangemTech -> createTangemTechModel()
ApiConfig.ID.StakeKit -> createStakeKitModel()
ApiConfig.ID.TangemPay -> createTangemPayModel()
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
}
}
private fun createExpressModel(): TestModel {
val environment = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
return TestModel(
id = ApiConfig.ID.Express,
expected = ApiEnvironmentConfig(
environment = environment,
baseUrl = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> "https://express.tangem.com/v1/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
},
headers = mapOf(
"api-key" to ProviderSuspend {
if (environment == ApiEnvironment.PROD) {
MockEnvironmentConfigStorage.EXPRESS_API_KEY
} else {
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
}
},
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"version" to ProviderSuspend { VERSION_NAME },
"system_version" to ProviderSuspend { "Android 16" },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
}
private fun createTangemTechModel(): TestModel {
return TestModel(
id = ApiConfig.ID.TangemTech,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",
headers = mapOf(
"card_id" to ProviderSuspend { APP_CARD_ID },
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
}
private fun createStakeKitModel(): TestModel {
return TestModel(
id = ApiConfig.ID.StakeKit,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend { STAKE_KIT_API_KEY },
"accept" to ProviderSuspend { "application/json" },
),
),
)
}
private fun createTangemPayModel(): TestModel {
return TestModel(
id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
),
),
)
}
private fun createBlockAidSdkModel(): TestModel {
return TestModel(
id = ApiConfig.ID.BlockAid,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.blockaid.io/v0/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend { BLOCK_AID_API_KEY },
"accept" to ProviderSuspend { "application/json" },
"content-type" to ProviderSuspend { "application/json" },
),
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]
val charCondition = c == '\t' || c in '\u0020'..'\u007e'
if (!charCondition) {
return ""
}
}
return this
}
data class TestModel(val id: ApiConfig.ID, val expected: ApiEnvironmentConfig)
private companion object {
const val VERSION_NAME = "debug"
const val EXPRESS_USER_ID = "express_user_id"
const val EXPRESS_SESSION_ID = "express_session_id"
const val EXPRESS_REF_CODE = "express_ref_code"
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
const val APP_CARD_ID = "app_card_id"
const val APP_CARD_PUBLIC_KEY = "app_public_key"
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Model> = API_CONFIGS.map {
when (it) {
is Express -> createExpressModel()
is TangemTech -> createTangemTechModel()
is StakeKit -> createStakeKitModel()
is TangemPay -> createVisaModel()
is Attestation -> createAttestationModel()
is BlockAid -> createBlockAidSdkModel()
}
}
private fun createExpressModel(): Model {
val environment = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
return Model(
id = ApiConfig.ID.Express,
expected = ApiEnvironmentConfig(
environment = environment,
baseUrl = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> "https://express.tangem.com/v1/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
},
headers = mapOf(
"api-key" to ProviderSuspend {
if (environment == ApiEnvironment.PROD) {
MockEnvironmentConfigStorage.EXPRESS_API_KEY
} else {
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
}
},
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"version" to ProviderSuspend { VERSION_NAME },
"system_version" to ProviderSuspend { "Android 16" },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
}
private fun createTangemTechModel(): Model {
return Model(
id = ApiConfig.ID.TangemTech,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",
headers = mapOf(
"card_id" to ProviderSuspend { APP_CARD_ID },
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
}
private fun createStakeKitModel(): Model {
return Model(
id = ApiConfig.ID.StakeKit,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend { STAKE_KIT_API_KEY },
"accept" to ProviderSuspend { "application/json" },
),
),
)
}
private fun createVisaModel(): Model {
return Model(
id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
),
),
)
}
private fun createAttestationModel(): Model {
return Model(
id = ApiConfig.ID.Attestation,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/",
),
)
}
private fun createBlockAidSdkModel(): Model {
return Model(
id = ApiConfig.ID.BlockAid,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.blockaid.io/v0/",
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]
val charCondition = c == '\t' || c in '\u0020'..'\u007e'
if (!charCondition) {
return ""
}
}
return this
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.tester.presentation.environments.utils
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
/**
* Comparator for [ApiEnvironmentConfig] based on the priority of [ApiEnvironment].
*
[REDACTED_AUTHOR]
*/
internal object ApiEnvironmentComparator : Comparator<ApiEnvironmentConfig> {
private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith {
when (it) {
ApiEnvironment.DEV -> 0
ApiEnvironment.STAGE -> 1
ApiEnvironment.PROD -> 2
}
}
override fun compare(o1: ApiEnvironmentConfig, o2: ApiEnvironmentConfig): Int {
val priority1: Int = getPriority(environment = o1.environment)
val priority2: Int = getPriority(environment = o2.environment)
return priority1.compareTo(priority2)
}
private fun getPriority(environment: ApiEnvironment): Int {
return apiEnvironmentPriorityMap[environment] ?: error("Unknown environment: $environment")
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.feature.tester.impl.BuildConfig
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM
import com.tangem.feature.tester.presentation.environments.utils.ApiEnvironmentComparator
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableSet
@ -76,6 +77,7 @@ internal class EnvironmentsTogglesViewModel @Inject constructor(
url = config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }?.baseUrl
?: error("Current environment's url isn't found"),
environments = config.environmentConfigs
.sortedWith(ApiEnvironmentComparator)
.map { environmentConfig -> environmentConfig.environment.name }
.toImmutableSet(),
)
@ -96,22 +98,22 @@ internal class EnvironmentsTogglesViewModel @Inject constructor(
viewModelScope.launch {
val environment = ApiEnvironment.valueOf(name)
handleIfAttestationConfig(id = id, environment = environment)
if (id == ApiConfig.ID.TangemTech.name) {
handleTangemTechConfig(environment = environment)
}
mutableApiConfigsManager.changeEnvironment(id = id, environment = environment)
}
}
/** Special logic for [ApiConfig.ID.Attestation] */
private fun handleIfAttestationConfig(id: String, environment: ApiEnvironment) {
if (id == ApiConfig.ID.Attestation.name) {
cardSdkConfigRepository.setTangemApiProdEnvFlag(
flag = when (environment) {
ApiEnvironment.PROD -> true
ApiEnvironment.DEV -> false
else -> false
},
)
}
/** Special logic for [ApiConfig.ID.TangemTech] */
private fun handleTangemTechConfig(environment: ApiEnvironment) {
cardSdkConfigRepository.setTangemApiProdEnvFlag(
flag = when (environment) {
ApiEnvironment.PROD -> true
ApiEnvironment.DEV -> false
ApiEnvironment.STAGE -> false
},
)
}
}