Updated on 2026-08-14
This commit is contained in:
parent
03c5bdd053
commit
f829c90402
33 changed files with 1502 additions and 0 deletions
13
domain/marketing/build.gradle.kts
Normal file
13
domain/marketing/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.domain.marketing.models)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
8
domain/marketing/models/build.gradle.kts
Normal file
8
domain/marketing/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
data class MarketingBanner(
|
||||
val uiType: UiType,
|
||||
val text: String?,
|
||||
val iconUrl: String?,
|
||||
val iconAlign: IconAlign?,
|
||||
val bgColor: String?,
|
||||
val deeplink: String?,
|
||||
val isDismissible: Boolean,
|
||||
) {
|
||||
|
||||
enum class UiType { STANDALONE, LINKED_TO_PROVIDER }
|
||||
|
||||
enum class IconAlign { LEFT, RIGHT }
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class MarketingCampaign(
|
||||
val id: Int,
|
||||
val type: MarketingScreenType,
|
||||
val priority: Int,
|
||||
val startAt: String?,
|
||||
val endAt: String?,
|
||||
val minAmount: BigDecimal?,
|
||||
val maxAmount: BigDecimal?,
|
||||
val providerIds: List<String>?,
|
||||
val banner: MarketingBanner,
|
||||
val targets: List<MarketingCampaignTarget>,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
sealed interface MarketingCampaignTarget {
|
||||
|
||||
/**
|
||||
* token_details / staking / yield campaigns target a network + contract address.
|
||||
* [contractAddress] is `null` for native coins (the backend omits it), so a `null`/blank contract
|
||||
* matches the coin of that network.
|
||||
*/
|
||||
data class NetworkContract(val networkId: String, val contractAddress: String?) : MarketingCampaignTarget
|
||||
|
||||
/** markets_token campaigns target a CoinGecko token id. */
|
||||
data class CoingeckoId(val id: String) : MarketingCampaignTarget
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
/**
|
||||
* Screen-specific request context. swap/onramp carry the pair params sent to the backend; background types
|
||||
* carry the on-screen token identity used for client-side target matching (the request itself sends only [type]).
|
||||
*/
|
||||
sealed interface MarketingScreen {
|
||||
|
||||
val type: MarketingScreenType
|
||||
|
||||
data class Swap(
|
||||
val fromNetwork: String,
|
||||
val fromContractAddress: String,
|
||||
val toNetwork: String,
|
||||
val toContractAddress: String,
|
||||
) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.SWAP
|
||||
}
|
||||
|
||||
data class Onramp(
|
||||
val fromFiat: String,
|
||||
val toNetwork: String,
|
||||
val toContractAddress: String,
|
||||
) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.ONRAMP
|
||||
}
|
||||
|
||||
data class TokenDetails(val networkId: String, val contractAddress: String) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.TOKEN_DETAILS
|
||||
}
|
||||
|
||||
data class TokenMarkets(val coingeckoId: String) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.TOKEN_MARKETS
|
||||
}
|
||||
|
||||
data class Staking(val networkId: String, val contractAddress: String) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.STAKING
|
||||
}
|
||||
|
||||
data class Yield(val networkId: String, val contractAddress: String) : MarketingScreen {
|
||||
override val type: MarketingScreenType = MarketingScreenType.YIELD
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
enum class MarketingScreenType(val value: String) {
|
||||
SWAP("swap"),
|
||||
ONRAMP("onramp"),
|
||||
TOKEN_DETAILS("token_details"),
|
||||
TOKEN_MARKETS("markets_token"),
|
||||
STAKING("staking"),
|
||||
YIELD("yield"),
|
||||
;
|
||||
|
||||
/** Background types are ETag-cached; swap/onramp are always re-requested per pair selection. */
|
||||
val isCacheable: Boolean
|
||||
get() = this != SWAP && this != ONRAMP
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): MarketingScreenType? = entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.domain.marketing.models
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class MarketingScreenTypeTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN screen types WHEN read value THEN matches backend snake_case contract`() {
|
||||
// Assert
|
||||
assertThat(MarketingScreenType.SWAP.value).isEqualTo("swap")
|
||||
assertThat(MarketingScreenType.ONRAMP.value).isEqualTo("onramp")
|
||||
assertThat(MarketingScreenType.TOKEN_DETAILS.value).isEqualTo("token_details")
|
||||
assertThat(MarketingScreenType.TOKEN_MARKETS.value).isEqualTo("markets_token")
|
||||
assertThat(MarketingScreenType.STAKING.value).isEqualTo("staking")
|
||||
assertThat(MarketingScreenType.YIELD.value).isEqualTo("yield")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN known value WHEN fromValue THEN returns type ELSE null`() {
|
||||
// Assert
|
||||
assertThat(MarketingScreenType.fromValue("token_details")).isEqualTo(MarketingScreenType.TOKEN_DETAILS)
|
||||
assertThat(MarketingScreenType.fromValue("unknown")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN screen type WHEN isCacheable THEN only background types cached`() {
|
||||
// Assert
|
||||
assertThat(MarketingScreenType.SWAP.isCacheable).isFalse()
|
||||
assertThat(MarketingScreenType.ONRAMP.isCacheable).isFalse()
|
||||
assertThat(MarketingScreenType.TOKEN_DETAILS.isCacheable).isTrue()
|
||||
assertThat(MarketingScreenType.TOKEN_MARKETS.isCacheable).isTrue()
|
||||
assertThat(MarketingScreenType.STAKING.isCacheable).isTrue()
|
||||
assertThat(MarketingScreenType.YIELD.isCacheable).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
import arrow.core.Either
|
||||
|
||||
class DismissMarketingBannerUseCase(
|
||||
private val repository: MarketingRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(campaignId: Int): Either<Throwable, Unit> = Either.catch {
|
||||
repository.dismissBanner(campaignId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.marketing.models.MarketingCampaign
|
||||
import com.tangem.domain.marketing.models.MarketingCampaignTarget
|
||||
import com.tangem.domain.marketing.models.MarketingScreen
|
||||
import com.tangem.domain.marketing.models.MarketingScreenType
|
||||
import java.math.BigDecimal
|
||||
|
||||
class GetMarketingBannerUseCase(
|
||||
private val repository: MarketingRepository,
|
||||
private val featureToggles: MarketingFeatureToggles,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns campaigns for [screen], filtered (dismissed, target match, USD amount range) and sorted by priority.
|
||||
*
|
||||
* @param amountUsd USD equivalent of the entered amount (swap/onramp only). When null, the amount filter is skipped.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
screen: MarketingScreen,
|
||||
amountUsd: BigDecimal? = null,
|
||||
): Either<Throwable, List<MarketingCampaign>> {
|
||||
if (!featureToggles.isMarketingBannersEnabled) return Either.Right(emptyList())
|
||||
|
||||
return repository.getCampaigns(screen).map { campaigns ->
|
||||
val dismissed = repository.getDismissedBannerIds()
|
||||
campaigns.asSequence()
|
||||
.filterNot { it.id in dismissed }
|
||||
.filter { matchesTarget(it, screen) }
|
||||
.filter { matchesAmount(it, amountUsd) }
|
||||
.sortedBy { it.priority }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun matchesTarget(campaign: MarketingCampaign, screen: MarketingScreen): Boolean = when (screen) {
|
||||
is MarketingScreen.Swap, is MarketingScreen.Onramp -> true // matched server-side by pair params
|
||||
is MarketingScreen.TokenDetails -> matchesNetworkContract(campaign, screen.networkId, screen.contractAddress)
|
||||
is MarketingScreen.Staking -> matchesNetworkContract(campaign, screen.networkId, screen.contractAddress)
|
||||
is MarketingScreen.Yield -> matchesNetworkContract(campaign, screen.networkId, screen.contractAddress)
|
||||
is MarketingScreen.TokenMarkets -> campaign.targets.any { target ->
|
||||
target is MarketingCampaignTarget.CoingeckoId && target.id == screen.coingeckoId
|
||||
}
|
||||
}
|
||||
|
||||
private fun matchesNetworkContract(
|
||||
campaign: MarketingCampaign,
|
||||
networkId: String,
|
||||
contractAddress: String,
|
||||
): Boolean {
|
||||
return campaign.targets.any { target ->
|
||||
target is MarketingCampaignTarget.NetworkContract &&
|
||||
target.networkId == networkId &&
|
||||
contractAddressMatches(target = target.contractAddress, screen = contractAddress)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Native coins have no contract address: the backend sends `contractAddress: null` and the screen
|
||||
* passes an empty string, so blank/null on both sides is a native-coin match. Otherwise the
|
||||
* contracts must match case-insensitively.
|
||||
*/
|
||||
private fun contractAddressMatches(target: String?, screen: String): Boolean {
|
||||
val normalizedTarget = target?.takeIf { it.isNotBlank() }
|
||||
val normalizedScreen = screen.takeIf { it.isNotBlank() }
|
||||
return when {
|
||||
normalizedTarget == null && normalizedScreen == null -> true
|
||||
normalizedTarget != null && normalizedScreen != null ->
|
||||
normalizedTarget.equals(normalizedScreen, ignoreCase = true)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun matchesAmount(campaign: MarketingCampaign, amountUsd: BigDecimal?): Boolean {
|
||||
val isAmountScreen = campaign.type == MarketingScreenType.SWAP || campaign.type == MarketingScreenType.ONRAMP
|
||||
if (!isAmountScreen || amountUsd == null) return true
|
||||
|
||||
val min = campaign.minAmount
|
||||
val max = campaign.maxAmount
|
||||
if (min != null && amountUsd < min) return false
|
||||
if (max != null && amountUsd > max) return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
interface MarketingFeatureToggles {
|
||||
val isMarketingBannersEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.marketing.models.MarketingCampaign
|
||||
import com.tangem.domain.marketing.models.MarketingScreen
|
||||
|
||||
interface MarketingRepository {
|
||||
|
||||
/** Fetches campaigns for [screen]. Returns Right(emptyList()) when there is nothing to show (incl. 5xx without cache). */
|
||||
suspend fun getCampaigns(screen: MarketingScreen): Either<Throwable, List<MarketingCampaign>>
|
||||
|
||||
/** Ids of campaigns whose banner the user has dismissed (stored client-side). */
|
||||
suspend fun getDismissedBannerIds(): Set<Int>
|
||||
|
||||
suspend fun dismissBanner(campaignId: Int)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DismissMarketingBannerUseCaseTest {
|
||||
|
||||
private val repository: MarketingRepository = mockk()
|
||||
private val useCase = DismissMarketingBannerUseCase(repository)
|
||||
|
||||
@BeforeEach
|
||||
fun reset() = clearMocks(repository)
|
||||
|
||||
@Test
|
||||
fun `GIVEN campaign id WHEN invoke THEN repository dismiss called and Right returned`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.dismissBanner(7) } returns Unit
|
||||
|
||||
// Act
|
||||
val result = useCase(7)
|
||||
|
||||
// Assert
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { repository.dismissBanner(7) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package com.tangem.domain.marketing
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.marketing.models.MarketingBanner
|
||||
import com.tangem.domain.marketing.models.MarketingCampaign
|
||||
import com.tangem.domain.marketing.models.MarketingCampaignTarget
|
||||
import com.tangem.domain.marketing.models.MarketingScreen
|
||||
import com.tangem.domain.marketing.models.MarketingScreenType
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class GetMarketingBannerUseCaseTest {
|
||||
|
||||
private val repository: MarketingRepository = mockk()
|
||||
private val featureToggles: MarketingFeatureToggles = mockk()
|
||||
private val useCase = GetMarketingBannerUseCase(repository, featureToggles)
|
||||
|
||||
@BeforeEach
|
||||
fun reset() {
|
||||
clearMocks(repository, featureToggles)
|
||||
every { featureToggles.isMarketingBannersEnabled } returns true
|
||||
coEvery { repository.getDismissedBannerIds() } returns emptySet()
|
||||
}
|
||||
|
||||
private fun banner() = MarketingBanner(
|
||||
uiType = MarketingBanner.UiType.STANDALONE, text = null, iconUrl = null,
|
||||
iconAlign = null, bgColor = null, deeplink = null, isDismissible = true,
|
||||
)
|
||||
|
||||
private fun campaign(
|
||||
id: Int,
|
||||
type: MarketingScreenType,
|
||||
priority: Int,
|
||||
minAmount: BigDecimal? = null,
|
||||
maxAmount: BigDecimal? = null,
|
||||
targets: List<MarketingCampaignTarget> = emptyList(),
|
||||
) = MarketingCampaign(
|
||||
id = id,
|
||||
type = type,
|
||||
priority = priority,
|
||||
startAt = null,
|
||||
endAt = null,
|
||||
minAmount = minAmount,
|
||||
maxAmount = maxAmount,
|
||||
providerIds = null,
|
||||
banner = banner(),
|
||||
targets = targets,
|
||||
)
|
||||
|
||||
private val swapScreen = MarketingScreen.Swap("eth", "0xF", "btc", "0xT")
|
||||
private val tokenScreen = MarketingScreen.TokenDetails(networkId = "ethereum", contractAddress = "0xA0b8")
|
||||
private val stakingScreen = MarketingScreen.Staking(networkId = "ethereum", contractAddress = "0xA0b8")
|
||||
private val yieldScreen = MarketingScreen.Yield(networkId = "ethereum", contractAddress = "0xA0b8")
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle disabled WHEN invoke THEN empty without touching repository`() = runTest {
|
||||
// Arrange
|
||||
every { featureToggles.isMarketingBannersEnabled } returns false
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEmpty()
|
||||
coVerify(exactly = 0) { repository.getCampaigns(any()) }
|
||||
coVerify(exactly = 0) { repository.getDismissedBannerIds() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN several campaigns WHEN invoke THEN sorted by priority ascending`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 3),
|
||||
campaign(id = 2, type = MarketingScreenType.SWAP, priority = 1),
|
||||
campaign(id = 3, type = MarketingScreenType.SWAP, priority = 2),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(2, 3, 1).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dismissed id WHEN invoke THEN dismissed campaign filtered out`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getDismissedBannerIds() } returns setOf(2)
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 1),
|
||||
campaign(id = 2, type = MarketingScreenType.SWAP, priority = 2),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount below min WHEN invoke swap THEN campaign filtered out`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 1, minAmount = BigDecimal(50), maxAmount = BigDecimal(300)),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen, amountUsd = BigDecimal(25))
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount within range WHEN invoke swap THEN campaign kept`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 1, minAmount = BigDecimal(50), maxAmount = BigDecimal(300)),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen, amountUsd = BigDecimal(100))
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount above max WHEN invoke swap THEN campaign filtered out`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 1, minAmount = BigDecimal(50), maxAmount = BigDecimal(300)),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen, amountUsd = BigDecimal(500))
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null amount WHEN invoke swap THEN amount filter skipped`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(swapScreen) } returns listOf(
|
||||
campaign(id = 1, type = MarketingScreenType.SWAP, priority = 1, minAmount = BigDecimal(50)),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(swapScreen, amountUsd = null)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN background type WHEN invoke THEN only campaigns matching the on-screen token kept`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(tokenScreen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.TOKEN_DETAILS, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("ethereum", "0xA0b8")),
|
||||
),
|
||||
campaign(
|
||||
id = 2, type = MarketingScreenType.TOKEN_DETAILS, priority = 2,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("bitcoin", "0xOther")),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(tokenScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token markets screen WHEN invoke THEN only campaigns matching the coingecko id kept`() = runTest {
|
||||
// Arrange
|
||||
val marketsScreen = MarketingScreen.TokenMarkets(coingeckoId = "1696501400")
|
||||
coEvery { repository.getCampaigns(marketsScreen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.TOKEN_MARKETS, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.CoingeckoId("1696501400")),
|
||||
),
|
||||
campaign(
|
||||
id = 2, type = MarketingScreenType.TOKEN_MARKETS, priority = 2,
|
||||
targets = listOf(MarketingCampaignTarget.CoingeckoId("other")),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(marketsScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN staking screen WHEN invoke THEN only campaigns matching the on-screen token kept`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(stakingScreen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.STAKING, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("ethereum", "0xA0b8")),
|
||||
),
|
||||
campaign(
|
||||
id = 2, type = MarketingScreenType.STAKING, priority = 2,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("bitcoin", "0xOther")),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(stakingScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN yield screen WHEN invoke THEN only campaigns matching the on-screen token kept`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getCampaigns(yieldScreen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.YIELD, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("ethereum", "0xA0b8")),
|
||||
),
|
||||
campaign(
|
||||
id = 2, type = MarketingScreenType.YIELD, priority = 2,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("bitcoin", "0xOther")),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(yieldScreen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN contract address differing only in case WHEN invoke THEN campaign matched`() = runTest {
|
||||
// Arrange
|
||||
val screen = MarketingScreen.TokenDetails(networkId = "ethereum", contractAddress = "0xA0B8")
|
||||
coEvery { repository.getCampaigns(screen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.TOKEN_DETAILS, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("ethereum", "0xa0b8")),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(screen)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN native coin screen WHEN campaign targets that coin with null contract THEN matched`() = runTest {
|
||||
// Arrange — native coin: screen contract is blank, target contract is null
|
||||
val screen = MarketingScreen.Yield(networkId = "bitcoin", contractAddress = "")
|
||||
coEvery { repository.getCampaigns(screen) } returns listOf(
|
||||
campaign(
|
||||
id = 1, type = MarketingScreenType.YIELD, priority = 1,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("bitcoin", contractAddress = null)),
|
||||
),
|
||||
campaign(
|
||||
id = 2, type = MarketingScreenType.YIELD, priority = 2,
|
||||
targets = listOf(MarketingCampaignTarget.NetworkContract("ethereum", contractAddress = null)),
|
||||
),
|
||||
).right()
|
||||
|
||||
// Act
|
||||
val result = useCase(screen)
|
||||
|
||||
// Assert — only the native coin of the on-screen network matches
|
||||
assertThat(result.getOrNull()?.map { it.id }).containsExactly(1)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue