diff --git a/Marathonfile b/Marathonfile
new file mode 100644
index 0000000000..91d15ba5fa
--- /dev/null
+++ b/Marathonfile
@@ -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
diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
index 96c353f84c..dfba2524d1 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
@@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
+import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
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.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity
@@ -86,6 +88,9 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection: () -> Unit = {},
) = before {
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()
runBlocking {
appPreferencesStore.editData { mutablePreferences ->
@@ -147,4 +152,8 @@ abstract class BaseTestCase : TestCase(
)
)
}
+
+ private companion object {
+ const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
index fe4c30d9a6..1095a7aa96 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
@@ -1,22 +1,31 @@
package com.tangem.common.utils
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
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
* @param scenarioName Name of the scenario to modify
* @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
*/
fun setWireMockScenarioState(
scenarioName: String,
state: String,
- baseUrl: String = "[REDACTED_ENV_URL]"
+ baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state")
@@ -46,8 +55,9 @@ fun setWireMockScenarioState(
/**
* 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 request = Request.Builder()
.url("$baseUrl/__admin/scenarios")
@@ -69,8 +79,9 @@ fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
/**
* 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("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
* @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 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
*/
fun resetWireMockScenarioState(
scenarioName: String,
initialState: String = "Started",
- baseUrl: String = "[REDACTED_ENV_URL]"
+ baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt
index 4e83789e83..6b59b1b8b2 100644
--- a/app/src/main/java/com/tangem/tap/TangemApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt
@@ -42,6 +42,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@@ -334,15 +335,23 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
- BlockchainSdkRetrofitBuilder.interceptors = listOf(
- createNetworkLoggingInterceptor(),
- ChuckerInterceptor(this),
- )
+ BlockchainSdkRetrofitBuilder.interceptors = buildList {
+ if (BuildConfig.MOCK_DATA_SOURCE) {
+ add(WireMockRedirectInterceptor())
+ }
+ add(createNetworkLoggingInterceptor())
+ add(ChuckerInterceptor(this@TangemApplication))
+ }
TangemApiServiceSettings.addInterceptors(
- createNetworkLoggingInterceptor(),
- ChuckerInterceptor(this),
- NetworkLogsSaveInterceptor(appLogsStore),
+ *buildList {
+ if (BuildConfig.MOCK_DATA_SOURCE) {
+ add(WireMockRedirectInterceptor())
+ }
+ add(createNetworkLoggingInterceptor())
+ add(ChuckerInterceptor(this@TangemApplication))
+ add(NetworkLogsSaveInterceptor(appLogsStore))
+ }.toTypedArray(),
)
}
diff --git a/app/src/mocked/res/xml/network_security_config.xml b/app/src/mocked/res/xml/network_security_config.xml
index 52c44ac992..a4929411f1 100644
--- a/app/src/mocked/res/xml/network_security_config.xml
+++ b/app/src/mocked/res/xml/network_security_config.xml
@@ -6,4 +6,9 @@
+
+
+ 10.0.2.2
+ localhost
+
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
index 2c3b8604a0..f1f6854fce 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
@@ -18,6 +18,7 @@ import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.datasource.utils.addHeaders
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
@@ -77,6 +78,7 @@ internal class RetrofitApiBuilder @Inject constructor(
.client(
OkHttpClient.Builder()
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
+ .applyWireMockRedirect()
.let {
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 {
if (apiConfigId in excludedApiForLogging) return this
diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt
new file mode 100644
index 0000000000..b246a027cb
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/tangem-android-tools b/tangem-android-tools
index ab6aee1d49..dc9df17919 160000
--- a/tangem-android-tools
+++ b/tangem-android-tools
@@ -1 +1 @@
-Subproject commit ab6aee1d49ac0cfca644330a525dced725d5a257
+Subproject commit dc9df17919126bce93482559fe26602f9801eb42