Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-07 18:58:42 +04:00
parent 0a8e78877d
commit 06263e1193
11 changed files with 360 additions and 436 deletions

View file

@ -10,12 +10,8 @@ import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.*
import com.tangem.domain.models.wallet.UserWalletId
import java.util.UUID
/**
* Use case for adding a new crypto portfolio account
@ -42,7 +38,7 @@ class AddCryptoPortfolioUseCase(
userWalletId: UserWalletId,
accountName: AccountName,
icon: CryptoPortfolioIcon,
derivationIndex: Int,
derivationIndex: DerivationIndex,
): Either<Error, Account.CryptoPortfolio> = either {
val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex)
@ -62,11 +58,10 @@ class AddCryptoPortfolioUseCase(
userWalletId: UserWalletId,
accountName: AccountName,
icon: CryptoPortfolioIcon,
derivationIndex: Int,
derivationIndex: DerivationIndex,
): Account.CryptoPortfolio {
// TODO: [REDACTED_JIRA]
return Account.CryptoPortfolio(
accountId = AccountId(userWalletId = userWalletId, value = UUID.randomUUID().toString()),
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
accountName = accountName,
accountIcon = icon,
derivationIndex = derivationIndex,
@ -77,7 +72,6 @@ class AddCryptoPortfolioUseCase(
groupType = TokensGroupType.NONE,
),
)
.getOrElse { raise(Error.AccountCreation(it)) }
}
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): Option<AccountList> {
@ -108,13 +102,6 @@ class AddCryptoPortfolioUseCase(
*/
sealed interface Error {
/**
* Error indicating that the account creation failed
*
* @property cause the underlying cause of the failure
*/
data class AccountCreation(val cause: Account.CryptoPortfolio.Error) : Error
/**
* Error indicating that the account list requirements were not met.
*

View file

@ -3,14 +3,13 @@ package com.tangem.domain.account.models
import arrow.core.Either
import arrow.core.left
import com.google.common.truth.Truth
import com.tangem.domain.account.utils.randomAccountId
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.account.utils.createAccount
import com.tangem.domain.account.utils.createAccounts
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
@ -19,7 +18,6 @@ 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
import kotlin.random.Random
/**
[REDACTED_AUTHOR]
@ -30,7 +28,7 @@ class AccountListTest {
@Test
fun mainAccount() {
// Arrange
val mainAccount = createAccount(isMain = true)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
val accountList = AccountList(
userWallet = mockk(),
@ -52,13 +50,13 @@ class AccountListTest {
// Arrange
val accountList = AccountList(
userWallet = mockk(),
accounts = createAccounts(count = 2),
accounts = createAccounts(userWalletId = userWalletId, count = 2),
totalAccounts = 2,
).getOrNull()!!
val fullAccountList = AccountList(
userWallet = mockk(),
accounts = createAccounts(20),
accounts = createAccounts(userWalletId = userWalletId, count = 20),
totalAccounts = 20,
).getOrNull()!!
@ -116,50 +114,41 @@ class AccountListTest {
expected = AccountList.Error.EmptyAccountsList.left(),
),
CreateTestModel(
accounts = setOf(createAccount(isMain = false)),
accounts = setOf(
createAccount(userWalletId = userWalletId, derivationIndex = 1),
),
expected = AccountList.Error.MainAccountNotFound.left(),
),
CreateTestModel(
accounts = setOf(
createAccount(isMain = true),
createAccount(isMain = true),
Account.CryptoPortfolio.createMainAccount(userWalletId),
Account.CryptoPortfolio.createMainAccount(userWalletId).copy(
accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
),
),
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
),
createAccount(isMain = true).let {
CreateTestModel(
accounts = setOf(it),
expected = AccountList(
userWallet = userWallet,
accounts = setOf(it),
totalAccounts = 1,
),
)
},
createAccounts(count = 20).let {
createAccounts(userWalletId = userWalletId, count = 1).let {
CreateTestModel(
accounts = it,
expected = AccountList(
userWallet = userWallet,
accounts = it,
totalAccounts = 20,
),
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1),
)
},
createAccounts(userWalletId = userWalletId, count = 20).let {
CreateTestModel(
accounts = it,
expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20),
)
},
CreateTestModel(
accounts = createAccounts(21),
accounts = createAccounts(userWalletId = userWalletId, count = 21),
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
),
CreateTestModel(
accounts = setOf(
createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
),
createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = false,
),
createAccount(userWalletId = userWalletId, derivationIndex = 0),
createAccount(userWalletId = userWalletId, derivationIndex = 1),
createAccount(userWalletId = userWalletId, derivationIndex = 1),
),
expected = AccountList.Error.DuplicateAccountIds.left(),
),
@ -190,12 +179,8 @@ class AccountListTest {
private fun provideTestModels() = listOf(
// region Add new account
run {
val mainAccount = createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
)
val newAccount = createAccount(isMain = false)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
val newAccount = createAccount(userWalletId = userWalletId, derivationIndex = 1)
PlusTestModel(
initial = AccountList(
@ -214,11 +199,7 @@ class AccountListTest {
// endregion
// region Replace existing account
run {
val mainAccount = createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!)
PlusTestModel(
@ -239,33 +220,12 @@ class AccountListTest {
PlusTestModel(
initial = AccountList(
userWallet = userWallet,
accounts = createAccounts(20),
accounts = createAccounts(userWalletId = userWalletId, count = 20),
totalAccounts = 20,
).getOrNull()!!,
toAdd = createAccount(isMain = false),
toAdd = createAccount(userWalletId = userWalletId, derivationIndex = 21),
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
),
PlusTestModel(
initial = AccountList(
userWallet = userWallet,
accounts = setOf(createAccount(isMain = true)),
totalAccounts = 1,
).getOrNull()!!,
toAdd = createAccount(isMain = true),
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
),
PlusTestModel(
initial = AccountList(
userWallet = userWallet,
accounts = setOf(
createAccount(isMain = true),
createAccount(isMain = false),
),
totalAccounts = 2,
).getOrNull()!!,
toAdd = createAccount(isMain = true),
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
),
)
}
@ -294,15 +254,8 @@ class AccountListTest {
private fun provideTestModels() = listOf(
// region Remove existing account
run {
val mainAccount = createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
)
val secondaryAccount = createAccount(
accountId = AccountId(value = "2", userWalletId = mockk()),
isMain = false,
)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2)
MinusTestModel(
initial = AccountList(
@ -321,14 +274,9 @@ class AccountListTest {
// endregion
// region Remove unexisting account
run {
val mainAccount = createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
)
val notInList = createAccount(
accountId = AccountId(value = "3", userWalletId = mockk()),
isMain = false,
)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
val notInList = createAccount(userWalletId = userWalletId, derivationIndex = 3)
MinusTestModel(
initial = AccountList(
userWallet = userWallet,
@ -346,7 +294,7 @@ class AccountListTest {
// endregion
// region EmptyAccountsList
run {
val mainAccount = createAccount(isMain = true)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
MinusTestModel(
initial = AccountList(
@ -361,14 +309,9 @@ class AccountListTest {
// endregion
// region MainAccountNotFound
run {
val mainAccount = createAccount(
accountId = AccountId(value = "1", userWalletId = mockk()),
isMain = true,
)
val secondaryAccount = createAccount(
accountId = AccountId(value = "2", userWalletId = mockk()),
isMain = false,
)
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2)
MinusTestModel(
initial = AccountList(
userWallet = userWallet,
@ -389,32 +332,8 @@ class AccountListTest {
val expected: Either<AccountList.Error, AccountList>,
)
private fun createAccounts(count: Int): Set<Account.CryptoPortfolio> {
return buildSet {
add(createAccount(isMain = true))
repeat(count - 1) {
add(createAccount(isMain = false))
}
}
}
private companion object {
private fun createAccount(
accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()),
accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
isMain: Boolean,
): Account.CryptoPortfolio {
return Account.CryptoPortfolio(
accountId = accountId,
name = "Test Account",
accountIcon = accountIcon,
derivationIndex = if (isMain) 0 else Random.nextInt(1, 21),
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
)
.getOrNull()!!
val userWalletId = UserWalletId("011")
}
}

View file

@ -7,11 +7,9 @@ import arrow.core.toOption
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.account.utils.createAccount
import com.tangem.domain.account.utils.createAccounts
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -20,8 +18,6 @@ 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.util.UUID
import kotlin.random.Random
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AddCryptoPortfolioUseCaseTest {
@ -29,47 +25,21 @@ class AddCryptoPortfolioUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = AddCryptoPortfolioUseCase(crudRepository)
private val userWalletId = UserWalletId("011")
private val userWallet = mockk<UserWallet>()
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository, userWallet)
every { userWallet.walletId } returns userWalletId
}
@Test
fun `invoke should add new crypto portfolio account to existing list`() = runTest {
// Arrange
val existingAccount = createAccount(
name = "Main Account",
derivationIndex = 0,
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId),
)
val accountList = AccountList(
userWallet = userWallet,
accounts = setOf(existingAccount),
totalAccounts = 1,
).getOrNull()!!
val fakeUUID = UUID.randomUUID()
mockkStatic(UUID::class)
every { UUID.randomUUID() } returns fakeUUID
val newAccount = createAccount(
accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId),
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = 1,
)
val newAccount = createNewAccount()
val accountList = AccountList.createEmpty(userWallet)
val updatedAccountList = (accountList + newAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } just Runs
// Act
val actual = useCase(
@ -89,30 +59,16 @@ class AddCryptoPortfolioUseCaseTest {
}
coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) }
unmockkStatic(UUID::class)
}
@Test
fun `invoke should create new account list if none exists`() = runTest {
// Arrange
val fakeUUID = UUID.randomUUID()
mockkStatic(UUID::class)
every { UUID.randomUUID() } returns fakeUUID
val newAccount = createAccount(
accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId),
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = 1,
)
val newAccount = createNewAccount()
val newAccountList = (AccountList.createEmpty(userWallet) + newAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns None
coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet
coEvery { crudRepository.saveAccounts(newAccountList) } just Runs
// Act
val actual = useCase(
@ -131,32 +87,6 @@ class AddCryptoPortfolioUseCaseTest {
crudRepository.getUserWallet(userWalletId)
crudRepository.saveAccounts(newAccountList)
}
unmockkStatic(UUID::class)
}
@Test
fun `invoke should return error if account creation fails`() = runTest {
// Act
val actual = useCase(
userWalletId = userWalletId,
accountName = AccountName.Main,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = -1,
)
// Assert
val expected = AddCryptoPortfolioUseCase.Error.AccountCreation(
cause = Account.CryptoPortfolio.Error.NegativeDerivationIndex,
).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerify(inverse = true) {
crudRepository.getAccounts(any())
crudRepository.getUserWallet(any())
crudRepository.saveAccounts(any())
}
}
@Test
@ -164,15 +94,11 @@ class AddCryptoPortfolioUseCaseTest {
// Arrange
val accountList = AccountList(
userWallet = userWallet,
accounts = createAccounts(count = 20),
accounts = createAccounts(userWalletId = userWalletId, count = 20),
totalAccounts = 20,
).getOrNull()!!
val newAccount = createAccount(
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = 1,
)
val newAccount = createNewAccount(derivationIndex = 21)
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
@ -202,12 +128,7 @@ class AddCryptoPortfolioUseCaseTest {
@Test
fun `invoke should return error if getAccounts throws exception`() = runTest {
// Arrange
val newAccount = createAccount(
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = 1,
)
val newAccount = createNewAccount()
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
@ -235,30 +156,8 @@ class AddCryptoPortfolioUseCaseTest {
@Test
fun `invoke should return error if saveAccounts throws exception`() = runTest {
// Arrange
val existingAccount = createAccount(
name = "Main Account",
derivationIndex = 0,
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId),
)
val accountList = AccountList(
userWallet = userWallet,
accounts = setOf(existingAccount),
totalAccounts = 1,
).getOrNull()!!
val fakeUUID = UUID.randomUUID()
mockkStatic(UUID::class)
every { UUID.randomUUID() } returns fakeUUID
val newAccount = createAccount(
accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId),
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = 1,
)
val newAccount = createNewAccount()
val accountList = AccountList.createEmpty(userWallet)
val updatedAccountList = (accountList + newAccount).getOrNull()!!
val exception = IllegalStateException("Test error")
@ -267,7 +166,7 @@ class AddCryptoPortfolioUseCaseTest {
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
useCase(
val actual = useCase(
userWalletId = userWalletId,
accountName = newAccount.name,
icon = newAccount.icon,
@ -275,8 +174,8 @@ class AddCryptoPortfolioUseCaseTest {
)
// Assert
// val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left()
// Truth.assertThat(actual).isEqualTo(expected)
val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
@ -286,32 +185,17 @@ class AddCryptoPortfolioUseCaseTest {
coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) }
}
private fun createAccounts(count: Int): Set<Account.CryptoPortfolio> {
return buildSet {
add(createAccount(derivationIndex = 0))
repeat(count - 1) {
add(createAccount())
}
private companion object {
val userWalletId = UserWalletId("011")
fun createNewAccount(derivationIndex: Int = 1): Account.CryptoPortfolio {
return createAccount(
userWalletId = userWalletId,
name = "New Account",
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = derivationIndex,
)
}
}
private fun createAccount(
accountId: AccountId? = null,
name: String = "Test Account",
icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex: Int = Random.nextInt(1, 21),
): Account.CryptoPortfolio {
return Account.CryptoPortfolio(
accountId = accountId ?: AccountId(value = UUID.randomUUID().toString(), userWalletId = userWalletId),
accountName = AccountName(name).getOrNull()!!,
accountIcon = icon,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
).getOrNull()!!
}
}

View file

@ -8,19 +8,17 @@ import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error
import com.tangem.domain.account.utils.randomAccountId
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.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 kotlin.random.Random
/**
[REDACTED_AUTHOR]
@ -31,25 +29,24 @@ class UpdateCryptoPortfolioUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository)
private val userWallet = mockk<UserWallet>()
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository)
clearMocks(crudRepository, userWallet)
every { userWallet.walletId } returns userWalletId
}
@Test
fun `invoke should update crypto portfolio account with new name`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(accountId = accountId, isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
)
.getOrNull()!!
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
val newAccountName = AccountName("New name").getOrNull()!!
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
@ -57,11 +54,9 @@ class UpdateCryptoPortfolioUseCaseTest {
val actual = useCase(accountId = accountId, accountName = newAccountName)
// Assert
val updatedAccount = account.copy(accountName = newAccountName)
val expected = updatedAccount.right()
Truth.assertThat(actual).isEqualTo(expected)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
@ -71,20 +66,15 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke should update crypto portfolio account with new icon`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(accountId = accountId, isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
)
.getOrNull()!!
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount(
value = CryptoPortfolioIcon.Icon.Star,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
@ -92,11 +82,9 @@ class UpdateCryptoPortfolioUseCaseTest {
val actual = useCase(accountId = accountId, icon = newAccountIcon)
// Assert
val updatedAccount = account.copy(accountIcon = newAccountIcon)
val expected = updatedAccount.right()
Truth.assertThat(actual).isEqualTo(expected)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
@ -106,21 +94,16 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke should update crypto portfolio account with new name and icon`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(accountId = accountId, isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
)
.getOrNull()!!
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
val newAccountName = AccountName("New name").getOrNull()!!
val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount(
value = CryptoPortfolioIcon.Icon.Star,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
@ -128,11 +111,9 @@ class UpdateCryptoPortfolioUseCaseTest {
val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon)
// Assert
val updatedAccount = account.copy(accountName = newAccountName, accountIcon = newAccountIcon)
val expected = updatedAccount.right()
Truth.assertThat(actual).isEqualTo(expected)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
coVerifyOrder {
crudRepository.getAccounts(userWalletId = userWalletId)
crudRepository.saveAccounts(accountList = updatedAccountList)
@ -142,15 +123,8 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke if name and icon are null`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(accountId = accountId, isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
)
.getOrNull()!!
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption()
@ -170,10 +144,11 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke if getAccounts throws exception`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
val newAccountName = AccountName("New name").getOrNull()!!
val exception = IllegalStateException("Test exception")
coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception
@ -192,8 +167,10 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke if getAccounts returns None`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex.Main,
)
val accountList = None
val newAccountName = AccountName("New name").getOrNull()!!
@ -214,18 +191,11 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke if getAccounts does not contain accountId`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(
accountId = AccountId(value = "another-account-id", userWalletId = mockk()),
isMain = true,
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex(1).getOrNull()!!,
)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
)
.getOrNull()!!
val newAccountName = AccountName("New name").getOrNull()!!
@ -245,17 +215,11 @@ class UpdateCryptoPortfolioUseCaseTest {
@Test
fun `invoke if saveAccounts throws exception`() = runTest {
// Arrange
val accountId = AccountId(value = "test-account-id", userWalletId = mockk())
val userWalletId = accountId.userWalletId
val account = createAccount(accountId = accountId, isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(account),
totalAccounts = 1,
).getOrNull()!!
val accountList = AccountList.createEmpty(userWallet = userWallet)
val accountId = accountList.mainAccount.accountId
val newAccountName = AccountName("New name").getOrNull()!!
val updatedAccount = account.copy(accountName = newAccountName)
val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName)
val updatedAccountList = (accountList + updatedAccount).getOrNull()!!
val exception = IllegalStateException("Save failed")
@ -276,23 +240,8 @@ class UpdateCryptoPortfolioUseCaseTest {
}
}
private fun createAccount(
accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()),
accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
isMain: Boolean,
): Account.CryptoPortfolio {
return Account.CryptoPortfolio(
accountId = accountId,
name = "Test Account",
accountIcon = accountIcon,
derivationIndex = if (isMain) 0 else Random.nextInt(1, 21),
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
)
.getOrNull()!!
private companion object {
val userWalletId = UserWalletId("011")
}
}

View file

@ -1,8 +1,41 @@
package com.tangem.domain.account.utils
fun randomAccountId(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return (1..length)
.map { chars.random() }
.joinToString("")
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.*
import com.tangem.domain.models.wallet.UserWalletId
import kotlin.random.Random
fun createAccounts(userWalletId: UserWalletId, count: Int): Set<Account.CryptoPortfolio> {
return buildSet {
add(Account.CryptoPortfolio.createMainAccount(userWalletId))
repeat(count - 1) {
val account = createAccount(userWalletId = userWalletId, derivationIndex = it + 1)
add(account)
}
}
}
fun createAccount(
userWalletId: UserWalletId,
name: String = "Test Account",
icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex: Int = Random.nextInt(1, 21),
): Account.CryptoPortfolio {
val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!
return Account.CryptoPortfolio(
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex),
accountName = AccountName(name).getOrNull()!!,
accountIcon = icon,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
)
}

View file

@ -2,10 +2,10 @@ 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.account.Account.CryptoPortfolio.Error.DerivationIndexError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@ -43,14 +43,14 @@ sealed interface Account {
override val accountId: AccountId,
override val name: AccountName,
val icon: CryptoPortfolioIcon,
val derivationIndex: Int,
val derivationIndex: DerivationIndex,
val isArchived: Boolean,
val cryptoCurrencyList: CryptoCurrencyList,
) : Account {
/** Indicates if the account is the main account */
val isMainAccount: Boolean
get() = derivationIndex == 0
get() = derivationIndex.isMain
/** Number of tokens in the account */
val tokensCount: Int
@ -97,15 +97,11 @@ sealed interface Account {
/** Error indicating that the account name is blank */
@Serializable
data class AccountNameError(val cause: AccountName.Error) : Error {
override fun toString(): String = cause.toString()
}
data class AccountNameError(val cause: AccountName.Error) : Error
/** Error indicating that the derivation index is negative */
@Serializable
data object NegativeDerivationIndex : Error {
override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative"
}
data class DerivationIndexError(val cause: DerivationIndex.Error) : Error
}
companion object {
@ -130,7 +126,8 @@ sealed interface Account {
cryptoCurrencyList: CryptoCurrencyList,
): Either<Error, CryptoPortfolio> {
return either {
val accountName = AccountName(name).mapLeft(::AccountNameError).bind()
val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind()
val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind()
invoke(
accountId = accountId,
@ -140,7 +137,6 @@ sealed interface Account {
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
.bind()
}
}
@ -159,22 +155,18 @@ sealed interface Account {
accountId: AccountId,
accountName: AccountName,
accountIcon: CryptoPortfolioIcon,
derivationIndex: Int,
derivationIndex: DerivationIndex,
isArchived: Boolean,
cryptoCurrencyList: CryptoCurrencyList,
): Either<Error, CryptoPortfolio> {
return either {
ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex }
CryptoPortfolio(
accountId = accountId,
name = accountName,
icon = accountIcon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
}
): CryptoPortfolio {
return CryptoPortfolio(
accountId = accountId,
name = accountName,
icon = accountIcon,
derivationIndex = derivationIndex,
isArchived = isArchived,
cryptoCurrencyList = cryptoCurrencyList,
)
}
/**
@ -183,12 +175,16 @@ sealed interface Account {
* @param userWalletId the ID of the user wallet
*/
fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio {
// TODO: [REDACTED_JIRA]
val derivationIndex = DerivationIndex.Main
return CryptoPortfolio(
accountId = AccountId(userWalletId = userWalletId, value = "main_account"),
accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
name = AccountName.Main,
icon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = 0,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),

View file

@ -1,7 +1,10 @@
package com.tangem.domain.models.account
import com.tangem.common.extensions.toByteArray
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.extensions.toHexString
import kotlinx.serialization.Serializable
import java.security.MessageDigest
/**
* Represents a unique identifier for an account
@ -10,7 +13,26 @@ import kotlinx.serialization.Serializable
* @property userWalletId the identifier of the user wallet associated with the account
*/
@Serializable
data class AccountId(
data class AccountId private constructor(
val value: String,
val userWalletId: UserWalletId,
)
) {
companion object {
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
/**
* Creates a unique account identifier for a crypto portfolio
*
* @param userWalletId the identifier of the user wallet
* @param derivationIndex the derivation index used to generate the identifier
*/
fun forCryptoPortfolio(userWalletId: UserWalletId, derivationIndex: DerivationIndex): AccountId {
val input = userWalletId.value + derivationIndex.value.toByteArray()
val value = sha256Digest.digest(input).toHexString()
return AccountId(value = value, userWalletId = userWalletId)
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.serialization.Serializable
/**
* Represents a derivation index for accounts, ensuring validity and providing utility methods
*
* @property value the integer value of the derivation index
*
[REDACTED_AUTHOR]
*/
@Serializable
data class DerivationIndex private constructor(
val value: Int,
) {
/** Checks if the derivation index corresponds to the main account */
val isMain: Boolean
get() = value == MAIN_ACCOUNT_DERIVATION_INDEX
/**
* Represents possible errors that can occur when creating a [DerivationIndex]
*/
@Serializable
sealed interface Error {
/** Error indicating that the provided derivation index [derivationIndex] is invalid */
@Serializable
data class NegativeDerivationIndex(val derivationIndex: Int) : Error {
override fun toString(): String {
return "${this::class.simpleName}: Derivation index cannot be negative: $derivationIndex"
}
}
}
companion object {
private const val MAIN_ACCOUNT_DERIVATION_INDEX = 0
/** Predefined instance of [DerivationIndex] for the main account */
val Main: DerivationIndex = DerivationIndex(value = MAIN_ACCOUNT_DERIVATION_INDEX)
/**
* Factory method to create a [DerivationIndex] instance
*
* @param value the integer value of the derivation index
*
* @return Either an error if the value is invalid, or a valid [DerivationIndex] instance
*/
operator fun invoke(value: Int): Either<Error, DerivationIndex> = either {
ensure(value >= 0) { Error.NegativeDerivationIndex(derivationIndex = value) }
DerivationIndex(value)
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.domain.models.account
import com.google.common.truth.Truth
import com.tangem.domain.models.wallet.UserWalletId
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountIdTest {
@ParameterizedTest
@MethodSource("provideTestModels")
fun forCryptoPortfolio(model: ForCryptoPortfolioModel) {
// Arrange
val userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F")
// Act
val actual = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = model.derivationIndex)
// Assert
Truth.assertThat(actual.value).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex.Main,
expected = "4E39B13EA11E3B35339664A10BEF48F4AF752A1CC2200F79D23CB0FB3396C63F",
),
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex(1).getOrNull()!!,
expected = "7F22E71F8106783F0F2DAFCDE525E2F2A2281E864DDBE2FE668FA09329D563A2",
),
ForCryptoPortfolioModel(
derivationIndex = DerivationIndex(42).getOrNull()!!,
expected = "555C1E17A302659446C97393453B7C2B3246AF4DA082C56C28FB6EDD1A6606A4",
),
)
data class ForCryptoPortfolioModel(
val derivationIndex: DerivationIndex,
val expected: String,
)
}

View file

@ -115,38 +115,18 @@ class AccountTest {
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `invoke returns NegativeDerivationIndex`() {
// Arrange
val derivationIndex = -1
// Act
val actual = CryptoPortfolio(
accountId = mockk(),
name = "Test Account",
accountIcon = mockk(),
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = mockk(),
)
.leftOrNull()!!
// Assert
val expected = CryptoPortfolio.Error.NegativeDerivationIndex
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `invoke returns CryptoPortfolio`() {
// Act
val derivationIndex = DerivationIndex.Main
val actual = CryptoPortfolio(
accountId = AccountId(
value = "value",
accountId = AccountId.forCryptoPortfolio(
userWalletId = UserWalletId("011"),
derivationIndex = derivationIndex,
),
name = "Test Account",
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")),
derivationIndex = 0,
derivationIndex = derivationIndex.value,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
@ -165,24 +145,27 @@ class AccountTest {
fun createMainAccount() {
// Arrange
val userWalletId = UserWalletId("011")
val derivationIndex = DerivationIndex.Main
// Act
val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
// Assert
// TODO: [REDACTED_JIRA]
val expected = CryptoPortfolio(
accountId = AccountId(userWalletId = userWalletId, value = "main_account"),
accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
accountName = AccountName.Main,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = 0,
derivationIndex = derivationIndex,
isArchived = false,
cryptoCurrencyList = CryptoCurrencyList(
currencies = emptySet(),
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
).getOrNull()
)
Truth.assertThat(actual).isEqualTo(expected)
}
@ -194,11 +177,10 @@ class AccountTest {
derivationIndex: Int = 0,
currencies: Set<CryptoCurrency> = emptySet(),
): CryptoPortfolio {
val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!!
return CryptoPortfolio.invoke(
accountId = AccountId(
value = "value",
userWalletId = userWalletId,
),
accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex),
name = name,
accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId),
derivationIndex = derivationIndex,

View file

@ -0,0 +1,49 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
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
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DerivationIndexTest {
@Test
fun `isMain returns true only for main derivation index`() {
// Arrange
val main = DerivationIndex.Main
val notMain = DerivationIndex(1).getOrNull()!!
// Act & Assert
Truth.assertThat(main.isMain).isTrue()
Truth.assertThat(notMain.isMain).isFalse()
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun invoke(model: InvokeTestModel) {
// Act
val actual = DerivationIndex(model.index)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
InvokeTestModel(index = 0, expected = DerivationIndex.Main.right()),
InvokeTestModel(index = 5, expected = DerivationIndex(5).getOrNull()!!.right()),
InvokeTestModel(index = -1, expected = DerivationIndex.Error.NegativeDerivationIndex(-1).left()),
)
data class InvokeTestModel(
val index: Int,
val expected: Either<DerivationIndex.Error, DerivationIndex>,
)
}