Updated on 2026-08-14
This commit is contained in:
commit
e8fdcbbc31
3 changed files with 80 additions and 74 deletions
|
|
@ -12,6 +12,8 @@ import com.tangem.screens.tangempay.*
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.openTangemPay() {
|
fun BaseTestCase.openTangemPay() {
|
||||||
|
// Existing customer: callers set the `tangem_pay_eligibility` scenario to PaeraCustomer (in
|
||||||
|
// additionalBeforeSection, before this runs), which drives the checkCustomerWalletId mock -> Payment account.
|
||||||
step("Import hot wallet from Tangem Pay seed phrase (with access code)") {
|
step("Import hot wallet from Tangem Pay seed phrase (with access code)") {
|
||||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12, accessCode = TANGEM_PAY_ACCESS_CODE)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,18 @@ package com.tangem.datasource.utils
|
||||||
|
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
|
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
|
||||||
|
*
|
||||||
|
* When a request hits the tangem-api-mocks catch-all (an unmocked endpoint → HTTP 404 with the
|
||||||
|
* [MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER] body), it folds the failing endpoint (method + URL) into the
|
||||||
|
* response body. That body becomes [com.tangem.datasource.api.common.response.ApiResponseError.HttpException.errorBody]
|
||||||
|
* (see ResponseExt), whose data-class `toString()` is what appears in a failing test's stack trace — so the
|
||||||
|
* exact missing mapping is named instead of an opaque `HttpException(404, errorBody={"error":"not_found",…})`.
|
||||||
*/
|
*/
|
||||||
class WireMockRedirectInterceptor : Interceptor {
|
class WireMockRedirectInterceptor : Interceptor {
|
||||||
|
|
||||||
|
|
@ -17,25 +25,64 @@ class WireMockRedirectInterceptor : Interceptor {
|
||||||
val host = request.url.host
|
val host = request.url.host
|
||||||
val sanitizedOverride = override.trimEnd('/')
|
val sanitizedOverride = override.trimEnd('/')
|
||||||
|
|
||||||
if (host == WIREMOCK_REMOTE_HOST) {
|
val redirectedRequest = when {
|
||||||
val newUrl = url.replace(WIREMOCK_REMOTE_URL, sanitizedOverride)
|
host == WIREMOCK_REMOTE_HOST -> {
|
||||||
TangemLogger.d("WireMockRedirect: $url -> $newUrl")
|
val newUrl = url.replace(WIREMOCK_REMOTE_URL, sanitizedOverride)
|
||||||
return chain.proceed(request.newBuilder().url(newUrl).build())
|
TangemLogger.d("WireMockRedirect: $url -> $newUrl")
|
||||||
|
request.newBuilder().url(newUrl).build()
|
||||||
|
}
|
||||||
|
host in REDIRECTABLE_THIRD_PARTY_HOSTS -> {
|
||||||
|
val newUrl = url.replace("https://$host", "$sanitizedOverride/$host")
|
||||||
|
TangemLogger.d("WireMockRedirect (3p): $url -> $newUrl")
|
||||||
|
request.newBuilder().url(newUrl).build()
|
||||||
|
}
|
||||||
|
else -> request
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host in REDIRECTABLE_THIRD_PARTY_HOSTS) {
|
val response = chain.proceed(redirectedRequest)
|
||||||
val newUrl = url.replace("https://$host", "$sanitizedOverride/$host")
|
return response.withEndpointInMockNotConfiguredError(redirectedRequest)
|
||||||
TangemLogger.d("WireMockRedirect (3p): $url -> $newUrl")
|
|
||||||
return chain.proceed(request.newBuilder().url(newUrl).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If [this] is the tangem-api-mocks catch-all 404 ("Mocked endpoint not configured"), rewrite the error body
|
||||||
|
* to also carry the failing endpoint, so it surfaces in the resulting `HttpException.errorBody`. Non-404
|
||||||
|
* responses (incl. the POST JSON-RPC "unreachable" catch-all, which is handled gracefully and must not be
|
||||||
|
* altered) are returned untouched.
|
||||||
|
*/
|
||||||
|
private fun Response.withEndpointInMockNotConfiguredError(request: Request): Response {
|
||||||
|
if (code != HTTP_NOT_FOUND) return this
|
||||||
|
|
||||||
|
val original = runCatching { peekBody(PEEK_LIMIT_BYTES).string() }.getOrNull() ?: return this
|
||||||
|
if (!original.contains(MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER)) return this
|
||||||
|
|
||||||
|
val endpoint = "${request.method} ${request.url}".jsonEscaped()
|
||||||
|
val enrichedJson = original.indexOf('{').let { open ->
|
||||||
|
if (open < 0) {
|
||||||
|
"""{"message":"$MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER","endpoint":"$endpoint"}"""
|
||||||
|
} else {
|
||||||
|
original.substring(0, open + 1) + """"endpoint":"$endpoint",""" + original.substring(open + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentType = body?.contentType()
|
||||||
|
body?.close()
|
||||||
|
return newBuilder().body(enrichedJson.toResponseBody(contentType)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.jsonEscaped(): String = replace("\\", "\\\\").replace("\"", "\\\"")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WIREMOCK_REMOTE_HOST = "wiremock.tests-d.com"
|
private const val WIREMOCK_REMOTE_HOST = "wiremock.tests-d.com"
|
||||||
private const val WIREMOCK_REMOTE_URL = "https://$WIREMOCK_REMOTE_HOST"
|
private const val WIREMOCK_REMOTE_URL = "https://$WIREMOCK_REMOTE_HOST"
|
||||||
|
|
||||||
|
private const val HTTP_NOT_FOUND = 404
|
||||||
|
|
||||||
|
/** Bounded peek so a large real response is never fully buffered just to look for the marker. */
|
||||||
|
private const val PEEK_LIMIT_BYTES = 4_096L
|
||||||
|
|
||||||
|
/** Emitted by the tangem-api-mocks catch-all mapping for any endpoint without an explicit mapping. */
|
||||||
|
private const val MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER = "Mocked endpoint not configured"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upstream hosts that have no other override knob and should be funnelled into WireMock
|
* Upstream hosts that have no other override knob and should be funnelled into WireMock
|
||||||
* when [overriddenBaseUrl] is set. Each matched URL becomes `<override>/<host>/<original-path>`,
|
* when [overriddenBaseUrl] is set. Each matched URL becomes `<override>/<host>/<original-path>`,
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,24 @@ import com.tangem.datasource.api.common.config.ApiConfig
|
||||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.domain.models.account.BankCredentials
|
import com.tangem.domain.models.account.BankCredentials
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
|
||||||
import com.tangem.domain.models.kyc.KycStatus
|
|
||||||
import com.tangem.domain.models.pay.TangemPayCard
|
|
||||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
|
||||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.model.CustomerInfo
|
import com.tangem.domain.pay.model.CustomerInfo
|
||||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
import com.tangem.domain.visa.error.VisaApiError
|
import com.tangem.domain.visa.error.VisaApiError
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In MOCK env returns canned onboarding data so the Payment Account shows up as fully loaded on the main
|
* In MOCK env only skips the local-storage / NFC-signing enrollment steps (order ids, initial data). The
|
||||||
* screen (the entry point to the TangemPay details & cashback screens), and skips local-storage / signing
|
* customer-facing state — whether a wallet has Tangem Pay ([hasTangemPayInWallet]), KYC status, ACTIVE /
|
||||||
* enrollment; remaining server calls go to WireMock.
|
* INACTIVE, balances ([getCustomerInfo]) — is driven by the WireMock test scenario (authenticated with the
|
||||||
|
* synthetic tokens from [com.tangem.data.pay.store.MockAwareTangemPayStorage]) rather than hardcoded:
|
||||||
|
* - [hasTangemPayInWallet] delegates to the real repo, so the "existing customer" gate follows the
|
||||||
|
* checkCustomerWalletId mock (the `tangem_pay_eligibility` scenario: `Started` → 404/NotPaeraCustomer →
|
||||||
|
* no Payment account, `PaeraCustomer` → 200 → Payment account);
|
||||||
|
* - [getCustomerInfo] delegates to the real repo (WireMock), so KYC / customer-state scenarios take effect.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class MockAwareOnboardingRepository @Inject constructor(
|
internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
|
|
@ -55,10 +55,12 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
real.produceInitialData(userWalletId)
|
real.produceInitialData(userWalletId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
|
// Delegates to WireMock (via the real repo + synthetic storage tokens) so the customer state — KYC status,
|
||||||
if (isMockMode) return MOCK_CUSTOMER_INFO.right()
|
// ACTIVE/INACTIVE, balances — follows the test scenario instead of a hardcoded "always active" customer.
|
||||||
return real.getCustomerInfo(userWalletId)
|
// Note: this may be invoked even when `hasTangemPayInWallet` is false (e.g., onboarding/deeplink flows),
|
||||||
}
|
// so tests must provide the corresponding WireMock mappings.
|
||||||
|
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
|
||||||
|
real.getCustomerInfo(userWalletId)
|
||||||
|
|
||||||
override suspend fun getBankCredentials(
|
override suspend fun getBankCredentials(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
|
|
@ -111,10 +113,12 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
real.storeVirtualAccountOrderId(userWalletId, vaOrderId)
|
real.storeVirtualAccountOrderId(userWalletId, vaOrderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
|
// The "existing Tangem Pay customer" gate (decides whether an active Payment account — and accounts mode —
|
||||||
if (isMockMode) return true.right()
|
// appears). Delegates to WireMock's checkCustomerWalletId via the real repo (static token, no signing), so it
|
||||||
return real.hasTangemPayInWallet(userWalletId)
|
// is driven by the `tangem_pay_eligibility` scenario: `Started` (default) → 404/NotPaeraCustomer → no account;
|
||||||
}
|
// `PaeraCustomer` → 200 → account. Generic UI tests never set the scenario, so they stay Payment-free.
|
||||||
|
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
||||||
|
real.hasTangemPayInWallet(userWalletId)
|
||||||
|
|
||||||
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> =
|
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||||
real.checkCustomerEligibility()
|
real.checkCustomerEligibility()
|
||||||
|
|
@ -152,52 +156,5 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
private companion object {
|
private companion object {
|
||||||
const val MOCK_ORDER_ID = "mock-order-id"
|
const val MOCK_ORDER_ID = "mock-order-id"
|
||||||
const val MOCK_VA_ORDER_ID = "mock-va-order-id"
|
const val MOCK_VA_ORDER_ID = "mock-va-order-id"
|
||||||
|
|
||||||
const val MOCK_CUSTOMER_ID = "mock-customer-id"
|
|
||||||
const val MOCK_CARD_ID = "mock-card-id"
|
|
||||||
const val MOCK_PRODUCT_INSTANCE_ID = "mock-product-instance-id"
|
|
||||||
const val MOCK_CUSTOMER_WALLET_ADDRESS = "0x0000000000000000000000000000000000000002"
|
|
||||||
const val MOCK_TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
|
||||||
const val MOCK_POLYGON_CHAIN_ID = 137L
|
|
||||||
|
|
||||||
val MOCK_CUSTOMER_INFO = CustomerInfo(
|
|
||||||
customerId = MOCK_CUSTOMER_ID,
|
|
||||||
productInstances = listOf(
|
|
||||||
CustomerInfo.ProductInstance(
|
|
||||||
id = MOCK_PRODUCT_INSTANCE_ID,
|
|
||||||
cardId = MOCK_CARD_ID,
|
|
||||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
|
||||||
displayName = null,
|
|
||||||
actualCardLimit = null,
|
|
||||||
adminCardLimit = null,
|
|
||||||
status = CustomerInfo.ProductInstance.Status.ACTIVE,
|
|
||||||
specificationDataType = CustomerInfo.ProductInstance.SpecificationDataType.CARD,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cards = listOf(
|
|
||||||
CustomerInfo.CardInfo(
|
|
||||||
cardId = MOCK_CARD_ID,
|
|
||||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
|
||||||
lastFourDigits = "4242",
|
|
||||||
isPinSet = true,
|
|
||||||
images = emptyList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
kycStatus = KycStatus.APPROVED,
|
|
||||||
state = CustomerInfo.State.ACTIVE,
|
|
||||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
|
||||||
availableBalance = BigDecimal("123.45"),
|
|
||||||
currency = "USD",
|
|
||||||
),
|
|
||||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
|
||||||
id = "usd-coin",
|
|
||||||
chainId = MOCK_POLYGON_CHAIN_ID,
|
|
||||||
depositAddress = MOCK_CUSTOMER_WALLET_ADDRESS,
|
|
||||||
tokenContractAddress = MOCK_TOKEN_CONTRACT_ADDRESS,
|
|
||||||
balance = BigDecimal("123.45"),
|
|
||||||
),
|
|
||||||
availableForWithdrawal = BigDecimal("123.45"),
|
|
||||||
tariffPlan = null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue