From 3cd5c708c9609538c11bdc449a4d448570c03206 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 15:46:06 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/domain/models/account/Account.kt | 127 ++++++++++++ .../tangem/domain/models/account/AccountId.kt | 14 ++ .../domain/models/account/AccountName.kt | 59 ++++++ .../models/account/CryptoPortfolioIcon.kt | 83 +++----- .../domain/models/account/AccountNameTest.kt | 67 +++++++ .../domain/models/account/AccountTest.kt | 187 ++++++++++++++++++ .../models/account/CryptoPortfolioIconTest.kt | 172 ++++++++-------- 7 files changed, 564 insertions(+), 145 deletions(-) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt new file mode 100644 index 0000000000..398930b72e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -0,0 +1,127 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents an account + * +[REDACTED_AUTHOR] + */ +sealed interface Account { + + /** Unique identifier of the account */ + val accountId: AccountId + + /** Name of the account */ + val name: AccountName + + /** The identifier of the user wallet associated with the account */ + val userWalletId: UserWalletId + get() = accountId.userWalletId + + /** + * Represents a crypto portfolio account + * + * @property accountId unique identifier of the account + * @property name name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property isArchived indicates whether the account is archived + * @property cryptoCurrencyList list of tokens associated with the account + */ + data class CryptoPortfolio private constructor( + override val accountId: AccountId, + override val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: Int, + val isArchived: Boolean, + val cryptoCurrencyList: CryptoCurrencyList, + ) : Account { + + /** Indicates if the account is the main account */ + val isMainAccount: Boolean + get() = derivationIndex == 0 + + /** Number of tokens in the account */ + val tokensCount: Int + get() = cryptoCurrencyList.currencies.size + + /** Number of distinct networks in the account */ + val networksCount: Int + get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + + /** + * Represents a list of tokens in the account + * + * @property currencies set of cryptocurrencies in the account + * @property sortType sorting type for the tokens + * @property groupType grouping type for the tokens + */ + data class CryptoCurrencyList( + val currencies: Set, + val sortType: TokensSortType, + val groupType: TokensGroupType, + ) + + /** + * Represents possible errors when creating a crypto portfolio account + */ + sealed interface Error { + + /** Error indicating that the account name is blank */ + data class AccountNameError(val cause: AccountName.Error) : Error { + override fun toString(): String = cause.toString() + } + + /** Error indicating that the derivation index is negative */ + data object NegativeDerivationIndex : Error { + override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative" + } + } + + companion object { + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param name name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + name: String, + accountIcon: CryptoPortfolioIcon, + derivationIndex: Int, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): Either { + return either { + val accountName = AccountName(name).mapLeft(::AccountNameError).bind() + + ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex } + + CryptoPortfolio( + accountId = accountId, + name = accountName, + icon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + } + } + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt new file mode 100644 index 0000000000..2f5728579c --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents a unique identifier for an account + * + * @property value a unique string value that distinguishes this account + * @property userWalletId the identifier of the user wallet associated with the account + */ +data class AccountId( + val value: String, + val userWalletId: UserWalletId, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt new file mode 100644 index 0000000000..e51a8e5a30 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure + +/** + * Represents an account name + * + * @property value the validated account name as a string + * +[REDACTED_AUTHOR] + */ +data class AccountName private constructor( + val value: String, +) { + + /** + * Represents possible validation errors + */ + sealed interface Error { + + /** + * Error indicating that the account name is blank + */ + data object Empty : Error { + override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank" + } + + /** + * Error indicating that the account name exceeds the maximum allowed length + */ + data object ExceedsMaxLength : Error { + override fun toString(): String { + return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters" + } + } + } + + companion object { + + private const val MAX_LENGTH = 20 + + /** + * Factory method to create an `AccountName` instance. + * Validates the input string to ensure it is not blank and does not exceed the maximum length. + * + * @param value the input string representing the account name + */ + operator fun invoke(value: String): Either = either { + val trimmedValue = value.trim() + + ensure(trimmedValue.isNotBlank()) { Error.Empty } + ensure(trimmedValue.length <= MAX_LENGTH) { Error.ExceedsMaxLength } + + AccountName(value = trimmedValue) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt index 9846695785..15b906020b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt @@ -1,48 +1,26 @@ package com.tangem.domain.models.account -import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofCustomAccount +import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofDefaultCustomAccount import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofMainAccount +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable /** * Represents an icon for an [Account.CryptoPortfolio] account * - * @property type the type of the account icon + * @property value the type of the account icon * @property color the color of the account icon * - * @constructor [ofMainAccount], [ofCustomAccount] + * @constructor [ofMainAccount], [ofDefaultCustomAccount] * [REDACTED_AUTHOR] */ @Serializable data class CryptoPortfolioIcon private constructor( - val type: Type, + val value: Icon, val color: Color, ) { - /** - * Represents the type of an account icon. Can either be a specific [Icon] or a [Symbol] - */ - @Serializable - sealed interface Type { - - /** - * Represents a specific predefined icon type - * - * @property value the predefined [Icon] of the icon - */ - @Serializable - data class Icon(val value: CryptoPortfolioIcon.Icon) : Type - - /** - * Represents an icon with a letter - * - * @property value the letter used as the icon - */ - @Serializable - data class Symbol(val value: Char) : Type - } - /** * Enum class representing the icons of accounts */ @@ -91,59 +69,44 @@ data class CryptoPortfolioIcon private constructor( companion object { - private val defaultMainAccountType: Icon = Icon.Star - private val defaultMainAccountColor: Color = Color.Azure + private val defaultMainAccountIcon: Icon = Icon.Star + private val excludedCustomAccountIcons: Set = setOf(Icon.Letter, Icon.Star) + private const val HASH_MULTIPLIER = 31 /** - * Creates an [CryptoPortfolioIcon] for the Main account, ensuring the color is not in the excluded set. + * Creating a [CryptoPortfolioIcon] for the Main account with default values. + * The color is derived from the [UserWalletId]. * - * @param exclude excluded colors that are already used for main accounts + * @param userWalletId the ID of the user wallet */ - fun ofMainAccount(exclude: Set): CryptoPortfolioIcon { - val isDefaultColorBusy = defaultMainAccountColor in exclude + fun ofMainAccount(userWalletId: UserWalletId): CryptoPortfolioIcon { + val colors = Color.entries + val hash = userWalletId.value.fold(0) { acc, byte -> acc * HASH_MULTIPLIER + byte } - val color = if (isDefaultColorBusy) { - val colorsWithExcluded = Color.entries - exclude + val index = (hash and Int.MAX_VALUE) % colors.size + val color = colors[index] - val availableColors = if (colorsWithExcluded.isNotEmpty()) { - colorsWithExcluded - } else { - Color.entries - } - - availableColors.random() - } else { - defaultMainAccountColor - } - - return CryptoPortfolioIcon( - type = Type.Icon(value = defaultMainAccountType), - color = color, - ) + return CryptoPortfolioIcon(value = defaultMainAccountIcon, color = color) } /** * Creates an [CryptoPortfolioIcon] for a user account based on the account name - * - * @param accountName the name of the account, used to determine the letter for the icon */ - fun ofCustomAccount(accountName: String): CryptoPortfolioIcon { + fun ofDefaultCustomAccount(): CryptoPortfolioIcon { + val icon = (Icon.entries - excludedCustomAccountIcons).random() val color = Color.entries.random() - return CryptoPortfolioIcon( - type = Type.Symbol(value = accountName.first()), - color = color, - ) + return CryptoPortfolioIcon(value = icon, color = color) } /** * Creates a [CryptoPortfolioIcon] for a user account with a specific type and color * - * @param type the type of the account icon + * @param value the icon of the account * @param color the color of the account icon */ - fun ofCustomAccount(type: Type, color: Color): CryptoPortfolioIcon { - return CryptoPortfolioIcon(type = type, color = color) + fun ofCustomAccount(value: Icon, color: Color): CryptoPortfolioIcon { + return CryptoPortfolioIcon(value = value, color = color) } } } \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt new file mode 100644 index 0000000000..d68bc46ab9 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.left +import com.google.common.truth.Truth +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountNameTest { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: InvokeTestModel) { + // Act + val actual = AccountName(value = model.value) + + // Assert + actual + .onRight { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onLeft { + val expected = model.expected.leftOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + } + + private fun provideTestModels() = listOf( + InvokeTestModel( + value = "", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = " ", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = "a".repeat(21), + expected = AccountName.Error.ExceedsMaxLength.left(), + ), + "a".repeat(20).let { value -> + InvokeTestModel( + value = value, + expected = AccountName(value = value), + ) + }, + InvokeTestModel( + value = " name ", + expected = AccountName(value = "name"), + ), + InvokeTestModel( + value = "Main Account", + expected = AccountName(value = "Main Account"), + ), + ) + + data class InvokeTestModel( + val value: String, + val expected: Either, + ) +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt new file mode 100644 index 0000000000..d94c1a6bdf --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -0,0 +1,187 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountTest { + + @Test + fun `Account userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + + // Act + val actual = createCryptoPortfolioStub(userWalletId = userWalletId).userWalletId + + // Assert + Truth.assertThat(actual).isEqualTo(userWalletId) + } + + @Test + fun `CryptoPortfolio isMainAccount`() { + // Arrange + val derivationIndex0 = 0 + val derivationIndex1 = 1 + + // Act + val actual1 = createCryptoPortfolioStub(derivationIndex = derivationIndex0) + .isMainAccount + + val actual2 = createCryptoPortfolioStub(derivationIndex = derivationIndex1) + .isMainAccount + + // Assert + Truth.assertThat(actual1).isTrue() + Truth.assertThat(actual2).isFalse() + } + + @Test + fun `CryptoPortfolio tokensCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf(mockk()) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .tokensCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .tokensCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Test + fun `CryptoPortfolio networksCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf( + mockk { + every { network } returns mockk() + }, + ) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .networksCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .networksCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateCryptoPortfolio { + + @Test + fun `invoke returns AccountNameError`() { + // Arrange + val name = "" + + // Act + val actual = Account.CryptoPortfolio( + accountId = mockk(), + name = name, + accountIcon = mockk(), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = mockk(), + ) + .leftOrNull()!! + + // Assert + val expected = AccountNameError(cause = AccountName.Error.Empty) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `invoke returns NegativeDerivationIndex`() { + // Arrange + val derivationIndex = -1 + + // Act + val actual = Account.CryptoPortfolio( + accountId = mockk(), + name = "Test Account", + accountIcon = mockk(), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = mockk(), + ) + .leftOrNull()!! + + // Assert + val expected = Account.CryptoPortfolio.Error.NegativeDerivationIndex + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `invoke returns CryptoPortfolio`() { + // Act + val actual = Account.CryptoPortfolio( + accountId = AccountId( + value = "value", + userWalletId = UserWalletId("011"), + ), + name = "Test Account", + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + + // Assert + val expected = createCryptoPortfolioStub() + Truth.assertThat(actual).isEqualTo(expected) + } + } + + private fun createCryptoPortfolioStub( + userWalletId: UserWalletId = UserWalletId("011"), + name: String = "Test Account", + derivationIndex: Int = 0, + currencies: Set = emptySet(), + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio.invoke( + accountId = AccountId( + value = "value", + userWalletId = userWalletId, + ), + name = name, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = currencies, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt index 3706e6921d..ec55aabdcd 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt @@ -1,13 +1,14 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.account.CryptoPortfolioIcon.* +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon +import com.tangem.domain.models.wallet.UserWalletId import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject -import io.mockk.verify +import io.mockk.verifyOrder import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -23,159 +24,160 @@ class CryptoPortfolioIconTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfMainAccount { - @Test - fun `ofMainAccount with empty exclude`() { - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = emptySet()) - - // Assert - val expectedColor = Color.Azure - Truth.assertThat(actual.color).isEqualTo(expectedColor) - - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - } - @ParameterizedTest @MethodSource("provideTestModels") fun ofMainAccount(model: OfMainAccountModel) { - // Arrange - mockkObject(Random.Default) - - val size = (Color.entries.size - model.exclude.size).takeIf { it > 0 } ?: Color.entries.size - every { Random.nextInt(size) } returns model.randomNextInt - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = model.exclude) + val actual = CryptoPortfolioIcon.ofMainAccount(userWalletId = model.userWalletId) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(size) } - - unmockkObject(Random.Default) + val expectedIcon = Icon.Star + Truth.assertThat(actual.value).isEqualTo(expectedIcon) } private fun provideTestModels() = listOf( - // If the default color is already occupied (present in the exclude set), a random color from the - // remaining available colors will be selected for the main account icon. OfMainAccountModel( - exclude = setOf(Color.Azure), - randomNextInt = 0, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("1234567890abcdef"), + expectedColor = Color.Pattypan, ), OfMainAccountModel( - exclude = setOf(Color.Azure, Color.CaribbeanBlue), - randomNextInt = 0, - expectedColor = Color.entries[2], + userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F"), + expectedColor = Color.CandyGrapeFizz, ), - // If all colors are already occupied, a random one will be selected. OfMainAccountModel( - exclude = Color.entries.toSet(), - randomNextInt = 1, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("64A3791C180584C700EBECD6EAB36CBC34643BB449BC87761104C09F41DBCF3D"), + expectedColor = Color.PalatinateBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("01C061A99FCCEDA87933267EBAB3513592F83AD2E27BDA6EE5546BA96009D21F"), + expectedColor = Color.Pelati, + ), + OfMainAccountModel( + userWalletId = UserWalletId("6D387A8FA5D2AF95F601EBCA8736D73D2ED53159835D8C407FBD4BBB10290C8B"), + expectedColor = Color.CaribbeanBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("33FCD9B9982C31648C235AE55A29212D567ECD3BA24BE4227D1A01897ADBC959"), + expectedColor = Color.SweetDesire, + ), + OfMainAccountModel( + userWalletId = UserWalletId("197C8C5AA59270F3E9E1F30799A007D193DA596E6DC24C37D002C2EC203C2A0B"), + expectedColor = Color.VitalGreen, + ), + OfMainAccountModel( + userWalletId = UserWalletId("ACF90C18393828958B5E795771F0692A00D3D7ADC092F726AB4A7E3116DD6E6E"), + expectedColor = Color.Pattypan, ), ) } data class OfMainAccountModel( - val exclude: Set, - val randomNextInt: Int, + val userWalletId: UserWalletId, val expectedColor: Color, ) @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class OfCustomAccountBasedOnName { + inner class OfDefaultCustomAccount { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.BasedOnName) { + fun ofCustomAccount(model: OfDefaultCustomAccountModel) { // Arrange + val availableIcons = Icon.entries - setOf(Icon.Letter, Icon.Star) + mockkObject(Random.Default) - every { Random.nextInt(until = Color.entries.size) } returns model.randomNextInt + every { Random.nextInt(until = availableIcons.size) } returns model.randomIconIndex + every { Random.nextInt(until = Color.entries.size) } returns model.randomColorIndex // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(accountName = model.accountName) + val actual = CryptoPortfolioIcon.ofDefaultCustomAccount() // Assert - val expectedColor = model.expectedColor - Truth.assertThat(actual.color).isEqualTo(expectedColor) + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) - val expectedType = Type.Symbol(value = model.accountName.first()) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(until = Color.entries.size) } + verifyOrder { + Random.nextInt(until = availableIcons.size) + Random.nextInt(until = Color.entries.size) + } unmockkObject(Random.Default) } private fun provideTestModels() = listOf( - OfCustomAccountModel.BasedOnName( - accountName = "New account", - randomNextInt = 0, - expectedColor = Color.entries[0], + OfDefaultCustomAccountModel( + randomIconIndex = 0, + randomColorIndex = 0, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.User, color = Color.Azure), ), - OfCustomAccountModel.BasedOnName( - accountName = "Awesome", - randomNextInt = Color.entries.lastIndex, - expectedColor = Color.entries.last(), + OfDefaultCustomAccountModel( + randomIconIndex = 1, + randomColorIndex = 1, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Family, color = Color.CaribbeanBlue), + ), + OfDefaultCustomAccountModel( + randomIconIndex = Icon.entries.lastIndex - 2, + randomColorIndex = Color.entries.lastIndex, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Gift, color = Color.VitalGreen), ), ) } + data class OfDefaultCustomAccountModel( + val randomIconIndex: Int, + val randomColorIndex: Int, + val expected: CryptoPortfolioIcon, + ) + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfCustomAccountWithTypeAndColor { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.WithTypeAndColor) { + fun ofCustomAccount(model: OfCustomAccountModel) { // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(type = model.type, color = model.color) + val actual = CryptoPortfolioIcon.ofCustomAccount(value = model.icon, color = model.color) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) val expectedType = model.expectedType - Truth.assertThat(actual.type).isEqualTo(expectedType) + Truth.assertThat(actual.value).isEqualTo(expectedType) } private fun provideTestModels() = listOf( - OfCustomAccountModel.WithTypeAndColor( - type = Type.Icon(value = Icon.User), + OfCustomAccountModel( + icon = Icon.User, color = Color.CaribbeanBlue, - expectedType = Type.Icon(value = Icon.User), + expectedType = Icon.User, expectedColor = Color.CaribbeanBlue, ), - OfCustomAccountModel.WithTypeAndColor( - type = Type.Symbol(value = 'A'), + OfCustomAccountModel( + icon = Icon.Letter, color = Color.DullLavender, - expectedType = Type.Symbol(value = 'A'), + expectedType = Icon.Letter, + expectedColor = Color.DullLavender, + ), + OfCustomAccountModel( + icon = Icon.Star, + color = Color.DullLavender, + expectedType = Icon.Star, expectedColor = Color.DullLavender, ), ) } - sealed interface OfCustomAccountModel { - - data class BasedOnName( - val accountName: String, - val randomNextInt: Int, - val expectedColor: Color, - ) : OfCustomAccountModel - - data class WithTypeAndColor( - val type: Type, - val color: Color, - val expectedType: Type, - val expectedColor: Color, - ) : OfCustomAccountModel - } + data class OfCustomAccountModel( + val icon: Icon, + val color: Color, + val expectedType: Icon, + val expectedColor: Color, + ) } \ No newline at end of file