Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-03 17:04:46 +02:00
parent 9b0305efe9
commit 2453d63633
12 changed files with 702 additions and 5 deletions

View file

@ -71,8 +71,9 @@ When the user asks to **port** an iOS test to Android:
strings inside `step(...)`.
- **Each click is its own** `step("Click on '$x' button")`. Combining clicks into one step hides which
click failed in the Allure report.
- **Step naming**: `Click on 'X' button` (not "Tap X"); `Assert <thing> is displayed` (not "Check X
visible"). Keep it consistent with the existing suite.
- **Step naming**: `Click on 'X' button` (not "Tap X"); `Assert <thing> is displayed` / `is not displayed`.
Reviewers reject `is visible`, `does not exist`, `Check X visible` — the convention is **`is displayed` /
`is not displayed`** even though older tests in the file may still use the old phrasing (don't copy it).
- **No conditional `if (foo.isDisplayedSafely()) foo.performClick()`** for elements that are
deterministically present after `pm clear` — the `if` is dead code. Use a straight `performClick()`.
@ -131,5 +132,6 @@ Delete anything explaining WHAT a step does.
- **`reference/compose-traps.md`** — read when the screen uses `PullToRefreshBox`,
`TangemHoldToConfirmButton`, a Decompose model that fetches in `init {}`, or a hot-wallet import with
an access code. These have silent failure modes that look like passing tests.
- **`reference/running-and-debugging.md`** — read when building, installing, running a single test,
interpreting CLI/Allure output, using `@Ignore`, or driving WireMock scenarios.
- **`reference/running-and-debugging.md`** — read when building, installing, running tests (orchestrator
vs. raw `am instrument`), running against a local WireMock, interpreting CLI/Allure output, using
`@Ignore`, or driving WireMock scenarios.

View file

@ -41,6 +41,30 @@ so the hold gesture is silently swallowed: the button looks fine, the user holds
3. Snapshot again — byte-identical trees mean `onConfirm` didn't run.
4. Or check WireMock request stats for the downstream API call expected after `onConfirm`.
## Asserting enabled/disabled on a `Modifier.clickable` row
When a settings/list row puts `Modifier.clickable(enabled = isClickable, ...)` on the row **container**
(not the title `Text`), the enabled/disabled state lives on that container; the child Texts only carry
`testTag`/text. So `assertIsEnabled()` / `assertIsNotEnabled()` must target the container, matched by a
descendant text — not the title node itself.
Match the container in BOTH states with **click-action OR disabled-semantics**. Do NOT rely on
`hasClickAction()` alone: depending on the Compose version a `clickable(enabled = false)` row may not
expose an onClick action, so a `hasClickAction()`-only matcher finds no node and `assertIsNotEnabled()`
fails with "No node found".
```kotlin
import androidx.compose.ui.test.hasClickAction as withClickAction
import androidx.compose.ui.test.isNotEnabled as withDisabled
val row: KNode = child {
addSemanticsMatcher(withClickAction() or withDisabled()) // matches enabled AND disabled rows
hasAnyDescendant(withText(getResourceString(R.string.row_title))) // narrows to the specific row
useUnmergedTree = true
}
// enabled card: row.assertIsEnabled() ; disabled card: row.assertIsNotEnabled()
```
## `assertTextContains(x)` defaults to exact-segment match, not substring
`SemanticsNodeInteraction.assertTextContains(value, substring = false, ignoreCase = false)` defaults to

View file

@ -29,6 +29,77 @@ adb shell am instrument -w \
com.tangem.wallet.mocked.test/com.tangem.common.HiltTestRunner
```
## Harness: orchestrator vs. raw `am instrument`
The app is configured `execution = "ANDROIDX_TEST_ORCHESTRATOR"` (`app/build.gradle.kts`). The orchestrator
runs **each test method in its own process** (and can clear app data between them). It is still 100%
local — it runs on the same emulator; nothing remote about it.
Raw `adb shell am instrument` runs **all selected tests in one shared process**, which has two failure
modes that look like test bugs but aren't:
- Running several tests in one invocation → `IllegalStateException: There are multiple DataStores active
for the same file` mid-run. Run them one at a time (with `pm clear` between) if you must use raw
`am instrument`.
- Tests that re-scan the card inside **Card/Device Settings** (the "Scan card or ring" gate) →
`IllegalStateException: Tangem SDK is null after re-registering with foreground activity`. The existing
`ResetCardTest` crashes identically under raw `am instrument`. These only pass via the orchestrator.
**Prefer the orchestrator** (it's what CI/Marathon use). Run a class or method through Gradle:
```bash
./gradlew :app:connectedGoogleMockedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest
# or a single method: ...class=com.tangem.tests.DetailsTest#someTest
# or several classes: ...class=com.tangem.tests.DetailsTest,com.tangem.tests.SecurityModeTest
```
Gradle installs both APKs, runs via the orchestrator, then **uninstalls them** — so a following raw
`am instrument` reports `Unable to find instrumentation info`; reinstall both APKs first. Read results
from the JUnit XML (authoritative pass/fail counts), not just stdout:
```bash
ls -t app/build/outputs/androidTest-results/connected/mocked/flavors/google/*.xml | head -1
# inspect tests="…" failures="…" errors="…" skipped="…" and the <testcase>/<failure> nodes
```
## Running against local WireMock
Every instrumentation test runs with `ApiEnvironment.MOCK` (forced in `BaseTestCase.setupHooks`), so the
app's API base URLs point at `wiremock.tests-d.com` — i.e. tests **always** talk to WireMock, never the
real backend. By default that's the **remote** WireMock at `wiremock.tests-d.com`. To use a **local**
WireMock instead, pass `wiremockBaseUrl`: `WireMockRedirectInterceptor` then rewrites every
`wiremock.tests-d.com` request to your local instance.
Emulator addressing matters — `localhost` inside an emulator is the **emulator itself**, not your host:
- Use the host alias **`http://10.0.2.2:8081`** (no extra setup), **or**
- `http://localhost:8081` **with** `adb reverse tcp:8081 tcp:8081` run first.
Pass it through the orchestrator (recommended):
```bash
curl -s -X POST http://localhost:8081/__admin/scenarios/reset # start clean
./gradlew :app:connectedGoogleMockedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest \
-Pandroid.testInstrumentationRunnerArguments.wiremockBaseUrl=http://10.0.2.2:8081
```
(Raw `am instrument` equivalent: `-e wiremockBaseUrl http://10.0.2.2:8081` — subject to the harness
caveats above.)
**If a screen hangs / you get `ComposeNotIdleException` (infinite recomposition):** that usually means a
request the app made wasn't served (endless retry/loading), *not* a test bug. Ask WireMock what it
didn't match — this is the smoking gun:
```bash
curl -s http://localhost:8081/__admin/requests/unmatched | jq '.requests[] | "\(.method) \(.url)"'
```
`unmatched: 0` means the URL plumbing is correct and local WireMock served everything — look elsewhere
(harness/emulator) for the hang. A non-empty list names exactly which mapping (or scenario state) the
local instance is missing.
## Classify the result — Allure noise vs. real failure
After `pm clear`, `/data/user/0/<pkg>/files/original_screenshots` doesn't exist →
@ -51,7 +122,8 @@ Distinguish:
## WireMock cheatsheet
Local override is detected; otherwise hits remote. Default local port: `8081`.
Without a `wiremockBaseUrl` arg the app hits the **remote** WireMock (`wiremock.tests-d.com`); pass the
arg to redirect to a local instance (see "Running against local WireMock"). Default local port: `8081`.
```bash
# Set a scenario state — PUT, not POST

View file

@ -5,6 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDeviceSettingsScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.scanCardInDeviceSettings() {
step("Click on 'Scan card or ring' button") {
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
}
}
fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) {
step("Click on 'Scan card or ring' button") {
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }

View file

@ -13,6 +13,9 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasClickAction as withClickAction
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.isNotEnabled as withDisabled
class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DeviceSettingsPageObject>(semanticsProvider = semanticsProvider) {
@ -46,6 +49,19 @@ class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
useUnmergedTree = true
}
val securityModeRowTitle: KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE)
hasText(getResourceString(R.string.card_settings_security_mode))
useUnmergedTree = true
}
// Match the row container (not the title Text): enabled exposes a click action, disabled exposes disabled semantics.
val securityModeRow: KNode = child {
addSemanticsMatcher(withClickAction() or withDisabled())
hasAnyDescendant(withText(getResourceString(R.string.card_settings_security_mode)))
useUnmergedTree = true
}
fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child {
hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE)
useUnmergedTree = true

View file

@ -0,0 +1,27 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SecurityModePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SecurityModePageObject>(semanticsProvider = semanticsProvider) {
// Description only appears on the Security Mode screen — unambiguous "screen opened" signal.
val longTapOptionDescription: KNode = child {
hasText(getResourceString(R.string.details_manage_security_long_tap_description))
useUnmergedTree = true
}
val saveChangesButton: KNode = child {
hasText(getResourceString(R.string.common_save_changes))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSecurityModeScreen(function: SecurityModePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -6,9 +6,13 @@ import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.content.Firmware412MockContent
import com.tangem.tap.domain.sdk.mocks.content.S2CMockContent
import com.tangem.tap.domain.sdk.mocks.content.SingleCurrencyMockContent
import com.tangem.tap.domain.sdk.mocks.content.V3MockContent
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
@ -183,6 +187,144 @@ class DetailsTest : BaseTestCase() {
}
}
@AllureId("838")
@DisplayName("Details: (v3 multicurrency) fields")
@Test
fun v3MultiCurrencyDetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = V3MockContent)
}
onMainScreenTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
onDetailsScreen {
step("Assert 'Wallet connect' button is displayed") {
walletConnectButton.assertIsDisplayed()
}
step("Assert 'Buy Tangem card' button is displayed") {
buyTangemButton.assertIsDisplayed()
}
step("Assert 'App settings' button is displayed") {
appSettingsButton.assertIsDisplayed()
}
step("Assert 'Contact support' button is displayed") {
contactSupportButton.assertIsDisplayed()
}
step("Assert 'Terms of service' button is displayed") {
toSButton.assertIsDisplayed()
}
step("Assert app version is displayed") {
versionName.assertIsDisplayed()
}
}
}
@AllureId("9832")
@DisplayName("Details: (single currency) fields")
@Test
fun singleCurrencyDetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = SingleCurrencyMockContent)
}
onMainScreenTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
onDetailsScreen {
step("Assert 'Wallet connect' button is not displayed") {
walletConnectButton.assertIsNotDisplayed()
}
step("Assert 'Buy Tangem card' button is displayed") {
buyTangemButton.assertIsDisplayed()
}
step("Assert 'App settings' button is displayed") {
appSettingsButton.assertIsDisplayed()
}
step("Assert 'Contact support' button is displayed") {
contactSupportButton.assertIsDisplayed()
}
step("Assert 'Terms of service' button is displayed") {
toSButton.assertIsDisplayed()
}
step("Assert app version is displayed") {
versionName.assertIsDisplayed()
}
}
}
@AllureId("841")
@DisplayName("Details: (S2C) fields")
@Test
fun s2cDetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = S2CMockContent)
}
onMainScreenTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
onDetailsScreen {
step("Assert 'Wallet connect' button is not displayed") {
walletConnectButton.assertIsNotDisplayed()
}
step("Assert 'Buy Tangem card' button is displayed") {
buyTangemButton.assertIsDisplayed()
}
step("Assert 'App settings' button is displayed") {
appSettingsButton.assertIsDisplayed()
}
step("Assert 'Contact support' button is displayed") {
contactSupportButton.assertIsDisplayed()
}
step("Assert 'Terms of service' button is displayed") {
toSButton.assertIsDisplayed()
}
step("Assert app version is displayed") {
versionName.assertIsDisplayed()
}
}
}
// Parked: createWalletActions adds Sell for single-wallet cards with no isStart2Coin() check.
@Ignore("[REDACTED_JIRA]")
@AllureId("2869")
@DisplayName("Details: (S2C) no trade buttons and standard details")
@Test
fun s2cNoTradeButtonsDetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(mockContent = S2CMockContent)
}
onMainScreen {
step("Assert 'Buy' button is not displayed") {
buyButton.assertIsNotDisplayed()
}
step("Assert 'Sell' button is not displayed") {
sellButton.assertIsNotDisplayed()
}
step("Assert 'Swap' button is not displayed") {
swapButton.assertIsNotDisplayed()
}
}
onMainScreenTopBar {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
onDetailsScreen {
step("Assert 'Wallet connect' button is not displayed") {
walletConnectButton.assertIsNotDisplayed()
}
}
}
@AllureId("3647")
@DisplayName("Referral program: validate screen")
@Test

View file

@ -0,0 +1,68 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openDeviceSettingsScreen
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.scanCardInDeviceSettings
import com.tangem.screens.*
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 SecurityModeTest : BaseTestCase() {
@AllureId("2267")
@DisplayName("Security Mode: available for Twin cards")
@Test
fun securityModeOpensForTwinsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Twins, isTwinsCard = true)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Scan card in 'Device settings'") {
scanCardInDeviceSettings()
}
step("Assert 'Security mode' row is enabled") {
onDeviceSettingsScreen { securityModeRow.assertIsEnabled() }
}
step("Click on 'Security mode' button") {
onDeviceSettingsScreen { securityModeRow.performClick() }
}
onSecurityModeScreen {
step("Assert 'Long tap' option is displayed") {
longTapOptionDescription.assertIsDisplayed()
}
step("Assert 'Save changes' button is displayed") {
saveChangesButton.assertIsDisplayed()
}
}
}
@AllureId("9831")
@DisplayName("Security Mode: unavailable for single-capability cards")
@Test
fun securityModeRowDisabledForOtherCardsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()
}
step("Scan card in 'Device settings'") {
scanCardInDeviceSettings()
}
step("Assert 'Security mode' row title is displayed") {
onDeviceSettingsScreen { securityModeRowTitle.assertIsDisplayed() }
}
step("Assert 'Security mode' row is disabled") {
onDeviceSettingsScreen { securityModeRow.assertIsNotEnabled() }
}
}
}

View file

@ -36,6 +36,9 @@ object MockProvider {
MockOption("Backup Wallet") { BackupWalletMockContent },
MockOption("Dev Wallet") { DevWalletMockContent },
MockOption("Firmware 4.12") { Firmware412MockContent },
MockOption("V3 Multicurrency") { V3MockContent },
MockOption("Single Currency") { SingleCurrencyMockContent },
MockOption("Start2Coin") { S2CMockContent },
MockOption("Cobrand") { showCobrandConfigDialog(it) },
)
@ -99,6 +102,7 @@ object MockProvider {
ProductType.Note -> NoteMockContent
ProductType.Ring -> RingMockContent
ProductType.Twins -> TwinsMockContent
ProductType.Start2Coin -> S2CMockContent
else -> TODO()
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// Start2Coin (S2C): issuer "Start2Coin" trips isStart2Coin → single currency, WalletConnect hidden.
object S2CMockContent : MockContent {
override val cardDto = CardDTO(
cardId = "1198724260000000",
batchId = "CD04",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1671494400000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "Start2Coin",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Secp256k1),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, 106, 7, -77, -109, 39, 3, 80, 99, 31, 50, -40, -113, -81, -76, -21, 123, -60, 0, -121, -56, 126, 2, 123, 111, 80, 47, -37, 40, 119, -22, 33, 32),
chainCode = byteArrayOf(),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = true),
totalSignedHashes = 1,
remainingSignatures = 999999,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Start2Coin,
walletData = WalletData(blockchain = "BTC", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "1198724260000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// Single-currency card (XLM/ed25519, pre-4.0 firmware) → isMultiwalletAllowed false → WalletConnect hidden.
object SingleCurrencyMockContent : MockContent {
override val cardDto = CardDTO(
cardId = "0052000000000000",
batchId = "0052",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 3,
minor = 5,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Ed25519),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 1,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = WalletData(blockchain = "XLM", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "0052000000000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import java.util.Date
// v3 multicurrency card: single secp256k1 wallet on pre-4.0 firmware → isMultiwalletAllowed via the secp branch.
object V3MockContent : MockContent {
override val cardDto = CardDTO(
cardId = "0045000000000000",
batchId = "0045",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 3,
minor = 5,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(EllipticCurve.Secp256k1),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -27, -117, 23, 68, -3, 21, -109, 18, -67, -107, -42, -44, -16, -127, -53, 46, -109, -46, -51, 89, 119, 79, 111, 78, 62, -125, 72, 109, 8, 45, 59, 117),
chainCode = byteArrayOf(),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 1,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = WalletData(blockchain = "BTC", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap())
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "0045000000000000")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}