Updated on 2026-08-14
This commit is contained in:
parent
d9bc010cd0
commit
07ee441996
7 changed files with 729 additions and 0 deletions
|
|
@ -28,6 +28,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -1037,6 +1038,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
|||
assertThat(required.isResetApproval).isTrue()
|
||||
}
|
||||
|
||||
@Ignore("Check in final integrated approve test")
|
||||
@Test
|
||||
fun `should fallback to no-permission state when yield-module proxy address is unresolvable`() = runTest {
|
||||
// Given — yield store returns null (e.g. network unreachable on first resolve)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.approvalFullCallback] (the full give-approval flow callback).
|
||||
*
|
||||
* Covers the synchronously-verifiable side effects. The quote-reload path
|
||||
* ([SwapModel] `startLoadingQuotesFromLastState`) short-circuits because `dataState.amount` is null
|
||||
* in these setups, keeping the callbacks side-effect-bounded.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SwapModelApprovalFullCallbackTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token from-currency WHEN onApproveDone THEN adds contract address to in-progress`() = runTest {
|
||||
val model = createModel()
|
||||
val token = mockk<CryptoCurrency.Token>(relaxed = true) {
|
||||
every { contractAddress } returns "0xContract"
|
||||
}
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(currency = token),
|
||||
)
|
||||
|
||||
model.approvalFullCallback.onApproveDone()
|
||||
|
||||
verify(exactly = 1) { allowPermissionsHandler.addAddressToInProgress("0xContract") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null from-currency WHEN onApproveDone THEN does not add address to in-progress`() = runTest {
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(fromSwapCurrencyStatus = null)
|
||||
|
||||
model.approvalFullCallback.onApproveDone()
|
||||
|
||||
verify(exactly = 0) { allowPermissionsHandler.addAddressToInProgress(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN failure WHEN onApproveFailed THEN sends alert message`() = runTest {
|
||||
val model = createModel()
|
||||
|
||||
model.approvalFullCallback.onApproveFailed()
|
||||
|
||||
verify(exactly = 1) { messageSender.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onApproveClick THEN no interactions`() = runTest {
|
||||
val model = createModel()
|
||||
|
||||
// onApproveClick is intentionally a no-op; assert it does not crash.
|
||||
model.approvalFullCallback.onApproveClick()
|
||||
|
||||
verify(exactly = 0) { allowPermissionsHandler.addAddressToInProgress(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import io.mockk.coVerify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.approvalSelectorCallback] (the [PermissionDataState.PermissionSettings]
|
||||
* approval-type selector).
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SwapModelApprovalSelectorCallbackTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no current loaded state WHEN onApproveTypeSelected THEN no state change and no reload`() = runTest {
|
||||
val model = createModel()
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permission is not PermissionSettings WHEN onApproveTypeSelected THEN no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, PermissionDataState.Empty)),
|
||||
)
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same approve type WHEN onApproveTypeSelected THEN no state change and no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.LIMITED)),
|
||||
),
|
||||
)
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.LIMITED)
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN different approve type WHEN onApproveTypeSelected THEN updates state and triggers reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.LIMITED)),
|
||||
),
|
||||
)
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
val updated = model.dataState.getCurrentLoadedSwapState()
|
||||
val settings = updated?.permissionState as? PermissionDataState.PermissionSettings
|
||||
assertThat(settings).isNotNull()
|
||||
assertThat(settings!!.type).isEqualTo(ApproveType.UNLIMITED)
|
||||
assertThat(model.feeSelectorRepository.state.value).isEqualTo(FeeSelectorUM.Loading)
|
||||
coVerify(exactly = 1) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 1) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any state WHEN onCancelClick THEN no state change and no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings()),
|
||||
),
|
||||
)
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onCancelClick()
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.models.SwapPermissionUM
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.getApprovalParams].
|
||||
*
|
||||
* Builds [com.tangem.features.approval.api.GiveApprovalComponent.Params] only when:
|
||||
* - `uiState.permissionUM` is [SwapPermissionUM.PermissionRequired]
|
||||
* - `fromSwapCurrencyStatus` is non-null
|
||||
* - `feePaidCryptoCurrency` is non-null
|
||||
*/
|
||||
internal class SwapModelGetApprovalParamsTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permissionUM is not PermissionRequired THEN returns null`() {
|
||||
val model = createModel()
|
||||
// default uiState has permissionUM == Empty
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fromSwapCurrencyStatus THEN returns null`() {
|
||||
val model = createModel()
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = null,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null feePaidCryptoCurrency THEN returns null`() {
|
||||
val model = createModel()
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
feePaidCryptoCurrency = null,
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN give-approval prerequisites met THEN builds params with give footer`() {
|
||||
val model = createModel()
|
||||
val coldWallet = mockk<UserWallet.Cold>(relaxed = true)
|
||||
val currency = mockk<CryptoCurrency>(relaxed = true) { every { symbol } returns "DAI" }
|
||||
val status = mockk<CryptoCurrencyStatus>(relaxed = true)
|
||||
val from = swapCurrencyStatus(wallet = coldWallet, status = status, currency = currency)
|
||||
val feeStatus: CryptoCurrencyStatus = mockk(relaxed = true)
|
||||
val provider = swapProvider(name = "1inch")
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = feeStatus,
|
||||
selectedProvider = provider,
|
||||
amount = "12.5",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.userWalletId).isEqualTo(userWalletId)
|
||||
assertThat(params.cryptoCurrencyStatus).isEqualTo(status)
|
||||
assertThat(params.feeCryptoCurrencyStatus).isEqualTo(feeStatus)
|
||||
assertThat(params.amount).isEqualTo("12.5")
|
||||
assertThat(params.spenderAddress).isEqualTo("0xSpender")
|
||||
assertThat(params.isResetApproval).isFalse()
|
||||
// Cold wallet -> not hold-to-confirm
|
||||
assertThat(params.isHoldToConfirm).isFalse()
|
||||
assertThat(params.callback).isSameInstanceAs(model.approvalFullCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN hot wallet THEN isHoldToConfirm is true`() {
|
||||
val model = createModel()
|
||||
val hotWallet = mockk<UserWallet.Hot>(relaxed = true)
|
||||
val from = swapCurrencyStatus(wallet = hotWallet)
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
amount = "1",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.isHoldToConfirm).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN reset approval THEN isResetApproval is true`() {
|
||||
val model = createModel()
|
||||
val from = swapCurrencyStatus()
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = true, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
amount = "1",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.isResetApproval).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.getSelectApprovalTypeParams].
|
||||
*
|
||||
* Builds [com.tangem.features.approval.api.SelectApprovalTypeComponent.Params] from the current
|
||||
* `fromSwapCurrencyStatus` and the provider's loaded swap state. Returns `null` when prerequisites
|
||||
* are missing. `initialApproveType` is taken from [PermissionDataState.PermissionSettings.type] or
|
||||
* falls back to [ApproveType.LIMITED].
|
||||
*/
|
||||
internal class SwapModelGetSelectApprovalTypeParamsTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fromSwapCurrencyStatus THEN returns null`() {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = null,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, permissionSettings())),
|
||||
)
|
||||
|
||||
assertThat(model.getSelectApprovalTypeParams(provider)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN provider state is not QuotesLoadedState THEN returns null`() {
|
||||
val provider = swapProvider()
|
||||
val notLoaded: SwapState.EmptyAmountState = mockk(relaxed = true)
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
lastLoadedSwapStates = mapOf(provider to notLoaded),
|
||||
)
|
||||
|
||||
assertThat(model.getSelectApprovalTypeParams(provider)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN PermissionSettings present THEN uses its type as initialApproveType`() {
|
||||
val provider = swapProvider(name = "ParaSwap")
|
||||
val currency = mockk<CryptoCurrency>(relaxed = true) { every { symbol } returns "USDT" }
|
||||
val status = mockk<CryptoCurrencyStatus>(relaxed = true)
|
||||
val from = swapCurrencyStatus(status = status, currency = currency)
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.UNLIMITED)),
|
||||
),
|
||||
)
|
||||
|
||||
val params = model.getSelectApprovalTypeParams(provider)
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.userWalletId).isEqualTo(userWalletId)
|
||||
assertThat(params.cryptoCurrencyStatus).isEqualTo(status)
|
||||
assertThat(params.initialApproveType).isEqualTo(ApproveType.UNLIMITED)
|
||||
assertThat(params.callback).isSameInstanceAs(model.approvalSelectorCallback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.isPermissionNotNeeded].
|
||||
*
|
||||
* The getter is `true` when the current loaded swap state needs no approval:
|
||||
* - always when [PermissionDataState.Empty]
|
||||
* - additionally for [PermissionDataState.PermissionSettings] when the integrated-approve toggle is ON
|
||||
*/
|
||||
internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
private fun modelWithPermissionState(
|
||||
permissionState: PermissionDataState,
|
||||
isIntegratedApproveEnabled: Boolean,
|
||||
): SwapModel {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns isIntegratedApproveEnabled
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, permissionState)),
|
||||
)
|
||||
return model
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = false)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionSettings THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = false)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = false,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionSettings THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no current loaded state THEN permission is needed`() {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
val model = createModel()
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.stories.ShouldShowStoriesUseCase
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.usecase.CalculateAmountUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.feature.swap.domain.GetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.SetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.AllowPermissionsHandler
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
|
||||
import com.tangem.feature.swap.ui.transfer.SwapTransferStateBuilder
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
/**
|
||||
* Shared test infrastructure for [SwapModel] unit tests.
|
||||
*
|
||||
* Wires every constructor dependency as a relaxed MockK and stubs the init-block calls so that
|
||||
* constructing the model has no observable side effects. See `swap-model-test-plan.md` for details.
|
||||
*/
|
||||
internal abstract class SwapModelTestBase {
|
||||
|
||||
protected val router: Router = mockk(relaxed = true)
|
||||
protected val appRouter: AppRouter = mockk(relaxed = true)
|
||||
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
protected val analyticsErrorEventHandler: AnalyticsErrorHandler = mockk(relaxed = true)
|
||||
protected val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
protected val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase = mockk(relaxed = true)
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk(relaxed = true)
|
||||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk(relaxed = true)
|
||||
protected val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxed = true)
|
||||
protected val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase = mockk(relaxed = true)
|
||||
protected val shouldShowStoriesUseCase: ShouldShowStoriesUseCase = mockk(relaxed = true)
|
||||
protected val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk(relaxed = true)
|
||||
protected val swapInteractor: SwapInteractor = mockk(relaxed = true)
|
||||
protected val swapTransferInteractor: SwapTransferInteractor = mockk(relaxed = true)
|
||||
protected val swapTransferStateBuilder: SwapTransferStateBuilder = mockk(relaxed = true)
|
||||
protected val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||
protected val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase = mockk(relaxed = true)
|
||||
protected val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val tangemPayWithdrawWithSwapUseCase: TangemPayWithdrawWithSwapUseCase = mockk(relaxed = true)
|
||||
protected val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk(relaxed = true)
|
||||
protected val feeSelectorReloadTrigger: FeeSelectorReloadTrigger = mockk(relaxed = true)
|
||||
protected val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase = mockk(relaxed = true)
|
||||
protected val appsFlyerStore: AppsFlyerStore = mockk(relaxed = true)
|
||||
protected val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||
protected val initialCurrenciesResolver: InitialCurrenciesResolver = mockk(relaxed = true)
|
||||
protected val allowPermissionsHandler: AllowPermissionsHandler = mockk(relaxed = true)
|
||||
protected val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
|
||||
protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true)
|
||||
protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true)
|
||||
protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true)
|
||||
|
||||
private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true)
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase = mockk(relaxed = true)
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk(relaxed = true)
|
||||
|
||||
protected val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF")
|
||||
|
||||
/** Stubs init-block calls so [createModel] has no side effects. Call from `@BeforeEach`. */
|
||||
protected fun setUpBase() {
|
||||
val bridge = mockk<ChooseTokenBridge>(relaxed = true) {
|
||||
every { onCurrencyChosen } returns Channel()
|
||||
every { onClose } returns Channel()
|
||||
}
|
||||
every { chooseTokenBridgeFactory.create(any(), any(), any()) } returns bridge
|
||||
|
||||
every { getUserCountryUseCase.invokeSync() } returns UserCountry.Other("US").right()
|
||||
every { getBalanceHidingSettingsUseCase.invoke() } returns emptyFlow()
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||
coEvery { shouldShowStoriesUseCase.invokeSync(any()) } returns false
|
||||
coEvery { initialCurrenciesResolver.invoke(any(), any(), any(), any()) } returns (null to null)
|
||||
every { getSelectedAppCurrencyUseCase.invoke() } returns emptyFlow()
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
}
|
||||
|
||||
protected fun createParams(): SwapComponent.Params = SwapComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = null,
|
||||
screenSource = "Test",
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
protected fun createModel(): SwapModel = SwapModel(
|
||||
paramsContainer = MutableParamsContainer(createParams()),
|
||||
getUserCountryUseCase = getUserCountryUseCase,
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
chooseTokenBridgeFactory = chooseTokenBridgeFactory,
|
||||
router = router,
|
||||
appRouter = appRouter,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
analyticsErrorEventHandler = analyticsErrorEventHandler,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
updateDelayedCurrencyStatusUseCase = updateDelayedCurrencyStatusUseCase,
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = getFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getMinimumTransactionAmountSyncUseCase = getMinimumTransactionAmountSyncUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
shouldShowStoriesUseCase = shouldShowStoriesUseCase,
|
||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||
swapInteractor = swapInteractor,
|
||||
swapTransferInteractor = swapTransferInteractor,
|
||||
swapTransferStateBuilder = swapTransferStateBuilder,
|
||||
urlOpener = urlOpener,
|
||||
getAccountCurrencyStatusUseCase = getAccountCurrencyStatusUseCase,
|
||||
getPaymentAccountCryptoCurrencyStatusUseCase = getPaymentAccountCryptoCurrencyStatusUseCase,
|
||||
tangemPayWithdrawWithSwapUseCase = tangemPayWithdrawWithSwapUseCase,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
feeSelectorReloadTrigger = feeSelectorReloadTrigger,
|
||||
getTangemPayCustomerIdUseCase = getTangemPayCustomerIdUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
messageSender = messageSender,
|
||||
initialCurrenciesResolver = initialCurrenciesResolver,
|
||||
allowPermissionsHandler = allowPermissionsHandler,
|
||||
swapFeatureToggles = swapFeatureToggles,
|
||||
getSwapUiModeUseCase = getSwapUiModeUseCase,
|
||||
setSwapUiModeUseCase = setSwapUiModeUseCase,
|
||||
calculateAmountUseCase = calculateAmountUseCase,
|
||||
)
|
||||
|
||||
// region builders
|
||||
|
||||
protected fun swapProvider(
|
||||
id: String = "provider-1",
|
||||
name: String = "1inch",
|
||||
type: ExchangeProviderType = ExchangeProviderType.DEX,
|
||||
): SwapProvider = SwapProvider(
|
||||
providerId = id,
|
||||
name = name,
|
||||
type = type,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
protected fun permissionSettings(
|
||||
type: ApproveType = ApproveType.LIMITED,
|
||||
spender: String = "0xSpender",
|
||||
): PermissionDataState.PermissionSettings = PermissionDataState.PermissionSettings(
|
||||
type = type,
|
||||
spenderAddress = spender,
|
||||
)
|
||||
|
||||
protected fun quotesLoadedState(
|
||||
provider: SwapProvider,
|
||||
permissionState: PermissionDataState = PermissionDataState.Empty,
|
||||
): SwapState.QuotesLoadedState = mockk(relaxed = true) {
|
||||
every { swapProvider } returns provider
|
||||
every { this@mockk.permissionState } returns permissionState
|
||||
every {
|
||||
copy(
|
||||
fromTokenInfo = any(),
|
||||
toTokenInfo = any(),
|
||||
priceImpact = any(),
|
||||
preparedSwapConfigState = any(),
|
||||
permissionState = any(),
|
||||
swapDataModel = any(),
|
||||
currencyCheck = any(),
|
||||
validationResult = any(),
|
||||
minAdaValue = any(),
|
||||
swapProvider = any(),
|
||||
)
|
||||
} answers {
|
||||
quotesLoadedState(
|
||||
provider = provider,
|
||||
permissionState = arg(4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun swapCurrencyStatus(
|
||||
wallet: UserWallet = mockk(relaxed = true),
|
||||
status: CryptoCurrencyStatus = mockk(relaxed = true),
|
||||
currency: CryptoCurrency = mockk(relaxed = true),
|
||||
): SwapCurrencyStatus = mockk(relaxed = true) {
|
||||
every { userWallet } returns wallet
|
||||
every { this@mockk.status } returns status
|
||||
every { this@mockk.currency } returns currency
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue