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