Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-02 11:02:47 +02:00
parent aed32a47f1
commit 58e26ca5e6
8 changed files with 139 additions and 14 deletions

41
Marathonfile Normal file
View file

@ -0,0 +1,41 @@
name: "Tangem Android UI Tests"
outputDir: "build/reports/marathon"
testOutputTimeoutMillis: 180000 # 3 minutes
testBatchTimeoutMillis: 600000 # 10 minutes
vendorConfiguration:
type: "Android"
applicationApk: "app/build/outputs/apk/google/mocked/app-google-mocked.apk"
testApplicationApk: "app/build/outputs/apk/androidTest/google/mocked/app-google-mocked-androidTest.apk"
autoGrantPermission: true
applicationPmClear: true
testApplicationPmClear: true
# wiremockBaseUrl redirects API calls from wiremock.tests-d.com to local WireMock
# This is used on CI where each emulator has its own WireMock instance via adb reverse
instrumentationArgs:
wiremockBaseUrl: "http://localhost:8080"
allureConfiguration:
enabled: false # we collect allure via adb pull, not through marathon
poolingStrategy:
type: "omni"
batchingStrategy:
type: "isolate"
sortingStrategy:
type: "no-sorting"
retryStrategy:
type: "fixed-quota"
totalAllowedRetryQuota: 30
retryPerTestQuota: 1
flakinessStrategy:
type: "ignore"
analyticsConfiguration:
type: "disabled"
debug: false

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.printToLog import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport import com.kaspersky.components.alluresupport.withForcedAllureSupport
@ -19,6 +20,7 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity import com.tangem.tap.MainActivity
@ -86,6 +88,9 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection: () -> Unit = {}, additionalAfterSection: () -> Unit = {},
) = before { ) = before {
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE) Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
// Setup WireMock redirect for CI with local WireMock instances
val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
hiltRule.inject() hiltRule.inject()
runBlocking { runBlocking {
appPreferencesStore.editData { mutablePreferences -> appPreferencesStore.editData { mutablePreferences ->
@ -147,4 +152,8 @@ abstract class BaseTestCase : TestCase(
) )
) )
} }
private companion object {
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
}
} }

View file

@ -1,22 +1,31 @@
package com.tangem.common.utils package com.tangem.common.utils
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]"
/**
* Returns the WireMock base URL to use.
*/
private fun getWireMockBaseUrl(): String =
WireMockRedirectInterceptor.overriddenBaseUrl ?: DEFAULT_WIREMOCK_URL
/** /**
* Method uses to set WireMock scenario state * Method uses to set WireMock scenario state
* @param scenarioName Name of the scenario to modify * @param scenarioName Name of the scenario to modify
* @param state The target state to set (must be one of the scenario's possibleStates) * @param state The target state to set (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if state was set successfully, false otherwise * @return true if state was set successfully, false otherwise
*/ */
fun setWireMockScenarioState( fun setWireMockScenarioState(
scenarioName: String, scenarioName: String,
state: String, state: String,
baseUrl: String = "[REDACTED_ENV_URL]" baseUrl: String = getWireMockBaseUrl()
): Boolean { ): Boolean {
Timber.i("=== WireMock Scenario Set ===") Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state") Timber.i("Setting scenario '$scenarioName' to state: $state")
@ -46,8 +55,9 @@ fun setWireMockScenarioState(
/** /**
* Method checks accessibility of WireMock * Method checks accessibility of WireMock
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/ */
fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
val client = OkHttpClient() val client = OkHttpClient()
val request = Request.Builder() val request = Request.Builder()
.url("$baseUrl/__admin/scenarios") .url("$baseUrl/__admin/scenarios")
@ -69,8 +79,9 @@ fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
/** /**
* Method to reset all WireMock scenarios * Method to reset all WireMock scenarios
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/ */
fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
Timber.i("=== WireMock Scenarios Reset ===") Timber.i("=== WireMock Scenarios Reset ===")
Timber.i("Base URL: $baseUrl") Timber.i("Base URL: $baseUrl")
@ -105,13 +116,13 @@ fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
* Method to reset a specific WireMock scenario to its initial state * Method to reset a specific WireMock scenario to its initial state
* @param scenarioName Name of the scenario to reset * @param scenarioName Name of the scenario to reset
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates) * @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
* @param baseUrl WireMock base URL * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if reset was successful, false otherwise * @return true if reset was successful, false otherwise
*/ */
fun resetWireMockScenarioState( fun resetWireMockScenarioState(
scenarioName: String, scenarioName: String,
initialState: String = "Started", initialState: String = "Started",
baseUrl: String = "[REDACTED_ENV_URL]" baseUrl: String = getWireMockBaseUrl()
): Boolean { ): Boolean {
Timber.i("=== WireMock Scenario Reset ===") Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState") Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")

View file

@ -42,6 +42,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -334,15 +335,23 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler) ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) { if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
BlockchainSdkRetrofitBuilder.interceptors = listOf( BlockchainSdkRetrofitBuilder.interceptors = buildList {
createNetworkLoggingInterceptor(), if (BuildConfig.MOCK_DATA_SOURCE) {
ChuckerInterceptor(this), add(WireMockRedirectInterceptor())
) }
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
}
TangemApiServiceSettings.addInterceptors( TangemApiServiceSettings.addInterceptors(
createNetworkLoggingInterceptor(), *buildList {
ChuckerInterceptor(this), if (BuildConfig.MOCK_DATA_SOURCE) {
NetworkLogsSaveInterceptor(appLogsStore), add(WireMockRedirectInterceptor())
}
add(createNetworkLoggingInterceptor())
add(ChuckerInterceptor(this@TangemApplication))
add(NetworkLogsSaveInterceptor(appLogsStore))
}.toTypedArray(),
) )
} }

View file

@ -6,4 +6,9 @@
<certificates src="user" /> <certificates src="user" />
</trust-anchors> </trust-anchors>
</base-config> </base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config> </network-security-config>

View file

@ -18,6 +18,7 @@ import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addHeaders
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor import okhttp3.Interceptor
@ -77,6 +78,7 @@ internal class RetrofitApiBuilder @Inject constructor(
.client( .client(
OkHttpClient.Builder() OkHttpClient.Builder()
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig) .applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
.applyWireMockRedirect()
.let { .let {
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
} }
@ -181,6 +183,14 @@ internal class RetrofitApiBuilder @Inject constructor(
) )
} }
private fun OkHttpClient.Builder.applyWireMockRedirect(): OkHttpClient.Builder {
return if (BuildConfig.MOCK_DATA_SOURCE) {
addInterceptor(interceptor = WireMockRedirectInterceptor())
} else {
this
}
}
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder { private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
if (apiConfigId in excludedApiForLogging) return this if (apiConfigId in excludedApiForLogging) return this

View file

@ -0,0 +1,40 @@
package com.tangem.datasource.utils
import okhttp3.Interceptor
import okhttp3.Response
import timber.log.Timber
/**
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
*/
class WireMockRedirectInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val override = overriddenBaseUrl ?: return chain.proceed(chain.request())
val request = chain.request()
val url = request.url.toString()
if (url.contains(WIREMOCK_REMOTE_URL)) {
val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/'))
Timber.d("WireMockRedirect: $url -> $newUrl")
val newRequest = request.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
companion object {
private const val WIREMOCK_REMOTE_URL = "[REDACTED_ENV_URL]"
/**
* Override base URL for WireMock requests.
* When null (default), requests go to wiremock.tests-d.com.
* When set (e.g., "http://localhost:8080"), requests are redirected to local WireMock instance.
*/
var overriddenBaseUrl: String? = null
}
}

@ -1 +1 @@
Subproject commit ab6aee1d49ac0cfca644330a525dced725d5a257 Subproject commit dc9df17919126bce93482559fe26602f9801eb42