Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-13 16:42:47 +03:00
commit 43c11d0460
384 changed files with 19228 additions and 3308 deletions

View file

@ -6,14 +6,25 @@ import org.json.JSONObject
import com.tangem.utils.logging.TangemLogger
import java.util.concurrent.TimeUnit
// WC URIs embed a session symKey that lets anyone join/hijack the session — strip it before logging.
private val WC_SECRET_REGEX = Regex("(symKey(?:=|%3D))[^&\\s\"']+", RegexOption.IGNORE_CASE)
private fun redactWcSecrets(text: String): String =
WC_SECRET_REGEX.replace(text) { "${it.groupValues[1]}<redacted>" }
/**
* Requests a WalletConnect URI from the qa-tools service.
*
* Response shape (see qa-tools `/wc_uri` swagger):
* - 200: { success: true, wcUri: "wc:...", network, wallet, tangemDeepLink, timestamp, processingTime }
* - 5xx: { error, network, timestamp, errorType }
*/
fun getWcUri(
network: String = "ethereum",
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
TangemLogger.i("Getting WC URI for network: $network")
val url = "$baseUrl/wc_uri?network=$network"
TangemLogger.i("getWcUri: requesting $url")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
@ -23,37 +34,94 @@ fun getWcUri(
.build()
val request = Request.Builder()
.url("$baseUrl/wc_uri?network=$network")
.url(url)
.header("Accept", "application/json")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
TangemLogger.i("Response code: ${response.code}")
val body = response.body?.string().orEmpty()
val contentType = response.header("Content-Type") ?: "<missing>"
TangemLogger.i(
"getWcUri: HTTP ${response.code} ${response.message}, " +
"Content-Type=$contentType, body.length=${body.length}"
)
TangemLogger.i("getWcUri: raw body=${redactWcSecrets(body)}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
TangemLogger.i("Response body: $body")
val jsonObject = JSONObject(body)
if (jsonObject.getBoolean("success")) {
val wcUri = jsonObject.getString("wcUri")
TangemLogger.i("Got WC URI successfully: $wcUri")
wcUri
} else {
TangemLogger.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
null
if (!response.isSuccessful) {
TangemLogger.e("getWcUri: non-2xx response (${response.code}), body=${redactWcSecrets(body)}")
return@use null
}
if (body.isBlank()) {
TangemLogger.e("getWcUri: response body is empty")
return@use null
}
if (contentType.contains("text/html", ignoreCase = true) ||
body.trimStart().startsWith("<")
) {
val server = response.header("Server").orEmpty()
val wwwAuth = response.header("WWW-Authenticate").orEmpty()
val isCloudflareAccess = server.contains("cloudflare", ignoreCase = true) ||
wwwAuth.contains("Cloudflare-Access", ignoreCase = true) ||
body.contains("cloudflareaccess.com", ignoreCase = true)
if (isCloudflareAccess) {
TangemLogger.e(
"getWcUri: blocked by Cloudflare Access. The test runner is not " +
"authorized to reach $url — connect to the corporate VPN or " +
"configure a Cloudflare Access service token (CF-Access-Client-Id / " +
"CF-Access-Client-Secret headers) on the device."
)
} else {
TangemLogger.e(
"getWcUri: server returned HTML instead of JSON for $url. " +
"Verify [REDACTED_ENV_URL] and the current API in /docs."
)
}
return@use null
}
val jsonObject = try {
JSONObject(body)
} catch (e: Exception) {
TangemLogger.e("getWcUri: failed to parse body as JSON: ${redactWcSecrets(body)}", e)
return@use null
}
TangemLogger.i("getWcUri: response keys=${jsonObject.keys().asSequence().toList()}")
val success = jsonObject.optBoolean("success", false)
if (!success) {
val error = jsonObject.optString("error", "<no error field>")
val errorType = jsonObject.optString("errorType", "<no errorType field>")
TangemLogger.e(
"getWcUri: API success=false, error=$error, errorType=$errorType, body=${redactWcSecrets(body)}"
)
return@use null
}
val wcUri = jsonObject.optString("wcUri", "")
val tangemDeepLink = jsonObject.optString("tangemDeepLink", "")
val processingTime = jsonObject.optString("processingTime", "")
TangemLogger.i(
"getWcUri: parsed wcUri=${redactWcSecrets(wcUri)}, " +
"tangemDeepLink=${redactWcSecrets(tangemDeepLink)}, processingTime=$processingTime"
)
if (wcUri.isBlank() || !wcUri.startsWith("wc:")) {
TangemLogger.e(
"getWcUri: wcUri is missing or has unexpected format. " +
"wcUri='${redactWcSecrets(wcUri)}', body=${redactWcSecrets(body)}"
)
return@use null
}
wcUri
}
} catch (e: Exception) {
TangemLogger.e("Error getting WC URI", e)
TangemLogger.e("getWcUri: exception while requesting $url", e)
null
}
}

View file

@ -7,9 +7,10 @@ import androidx.test.core.app.ApplicationProvider
import io.github.kakaocup.kakao.intent.KIntent
fun openAppByDeepLink(deepLinkUri: String?) {
val deeplinkScheme = "tangem://wc?uri="
requireNotNull(deepLinkUri) { "openAppByDeepLink: deepLinkUri is null" }
val finalUri = Uri.parse(deepLinkUri)
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val intent = Intent(ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply {
val intent = Intent(ACTION_VIEW, finalUri).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK)
}

View file

@ -140,15 +140,6 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
step("Assert app URL is displayed") {
onWalletConnectDetailsBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert wallet icon is displayed") {
onWalletConnectDetailsBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert wallet title is displayed") {
onWalletConnectDetailsBottomSheet { walletTitle.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectDetailsBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Connected networks' title is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworksTitle.assertIsDisplayed() }
}

View file

@ -1,32 +1,36 @@
package com.tangem.tests
package com.tangem.tests.walletConnect
import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.*
import com.tangem.scenarios.checkWalletConnectBottomSheet
import com.tangem.scenarios.checkWalletConnectDetailsBottomSheet
import com.tangem.scenarios.checkWalletConnectScreen
import com.tangem.scenarios.openAppByDeepLink
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openWalletConnectScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectScreen
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class WalletConnectTest : BaseTestCase() {
class EthereumWalletConnectTest : BaseTestCase() {
@AllureId("3958")
@DisplayName("WC (React App): open session from deeplink on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionOnMainScreenTest() {
val dAppName = "React App"
val dAppName = "Tangem QA Tools"
val deepLinkUri = getWcUri()
setupHooks().run {
@ -36,11 +40,11 @@ class WalletConnectTest : BaseTestCase() {
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Create WC session buy deeplink") {
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
@ -59,7 +63,7 @@ class WalletConnectTest : BaseTestCase() {
openWalletConnectScreen()
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectScreen(withConnections = true)
}
}
@ -80,10 +84,9 @@ class WalletConnectTest : BaseTestCase() {
@AllureId("3959")
@DisplayName("WC (React App): open session from deeplink not on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionNotOnMainScreenTest() {
val dAppName = "React App"
val dAppName = "Tangem QA Tools"
val deepLinkUri = getWcUri()
setupHooks().run {
@ -97,11 +100,11 @@ class WalletConnectTest : BaseTestCase() {
openWalletConnectScreen()
checkWalletConnectScreen(false)
}
step("Create WC session buy deeplink") {
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
@ -114,7 +117,7 @@ class WalletConnectTest : BaseTestCase() {
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectScreen(withConnections = true)
}
}
@ -122,7 +125,7 @@ class WalletConnectTest : BaseTestCase() {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectDetailsBottomSheet(dAppName)
}
}
@ -136,11 +139,10 @@ class WalletConnectTest : BaseTestCase() {
}
@AllureId("3957")
@DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@DisplayName("WC (React App): open session from deeplink")
@Test
fun openWalletConnectSessionTest() {
val dAppName = "React App"
val dAppName = "Tangem QA Tools"
val packageName = BuildConfig.APPLICATION_ID
val deepLinkUri = getWcUri()
@ -154,11 +156,11 @@ class WalletConnectTest : BaseTestCase() {
step("Kill app") {
device.apps.kill(packageName)
}
step("Create WC session buy deeplink") {
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
@ -191,10 +193,9 @@ class WalletConnectTest : BaseTestCase() {
@AllureId("887")
@DisplayName("WC: open session by 'Paste from clipboard' button")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionByClipboardLinkTest() {
val dAppName = "React App"
val dAppName = "Tangem QA Tools"
val context = device.context
val deepLinkUri = getWcUri()
val packageName = BuildConfig.APPLICATION_ID
@ -225,7 +226,7 @@ class WalletConnectTest : BaseTestCase() {
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}

View file

@ -0,0 +1,301 @@
package com.tangem.tests.walletConnect
import android.Manifest
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.getWcUri
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setClipboardText
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkWalletConnectBottomSheet
import com.tangem.scenarios.checkWalletConnectDetailsBottomSheet
import com.tangem.scenarios.checkWalletConnectScreen
import com.tangem.scenarios.openAppByDeepLink
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openWalletConnectScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onScanQrScreen
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SolanaWalletConnectTest : BaseTestCase() {
@AllureId("4023")
@DisplayName("WC (Raydium): open session from deeplink on main screen")
@Test
fun openWalletConnectSessionOnMainScreenTest() {
val dAppName = "Tangem QA Tools"
val deepLinkUri = getWcUri("solana")
val scenarioState = "Solana"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Assert 'Connect' button is enabled") {
onWalletConnectBottomSheet { connectButton.assertIsEnabled() }
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectScreen(withConnections = true)
}
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
@AllureId("4024")
@DisplayName("WC (Raydium): open session from deeplink not on main screen")
@Test
fun openWalletConnectSessionNotOnMainScreenTest() {
val dAppName = "Tangem QA Tools"
val deepLinkUri = getWcUri("solana")
val scenarioState = "Solana"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
checkWalletConnectScreen(false)
}
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Check 'Wallet Connect' screen with connections") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectScreen(withConnections = true)
}
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectDetailsBottomSheet(dAppName)
}
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
@AllureId("4025")
@DisplayName("WC (Raydium): open session from deeplink")
@Test
fun openWalletConnectSessionTest() {
val dAppName = "Tangem QA Tools"
val packageName = BuildConfig.APPLICATION_ID
val deepLinkUri = getWcUri("solana")
val scenarioState = "Solana"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Kill app") {
device.apps.kill(packageName)
}
step("Create WC session by deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
@AllureId("4026")
@DisplayName("WC: open session by 'Paste from clipboard' button")
@Test
fun openWalletConnectSessionByClipboardLinkTest() {
val dAppName = "Tangem QA Tools"
val context = device.context
val deepLinkUri = getWcUri("solana")
val scenarioState = "Solana"
val packageName = BuildConfig.APPLICATION_ID
val permissionName = Manifest.permission.CAMERA
setupHooks(
additionalBeforeSection = {
device.uiDevice.executeShellCommand("pm grant $packageName $permissionName")
},
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
}
step("Set URI to clipboard") {
setClipboardText(context, deepLinkUri)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Wallet Connect' screen") {
openWalletConnectScreen()
}
step("Click 'New connection' button") {
onWalletConnectScreen { newConnectionButton.performClick() }
}
step("CLick 'Paste from clipboard' button") {
onScanQrScreen { pasteFromClipboardButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' bottom sheet") {
waitForIdle()
flakySafely(TestConstants.WAIT_UNTIL_TIMEOUT) {
checkWalletConnectBottomSheet()
}
}
step("Click on 'Connect' button") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Assert 'Connect' button is not displayed") {
waitForIdle()
onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() }
}
step("Check 'Wallet Connect' screen with connections") {
checkWalletConnectScreen(withConnections = true)
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect' button") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Check 'Wallet Connect' screen without connections") {
checkWalletConnectScreen(withConnections = false)
}
}
}
}