From 34f07ed52d1be4d82fa9cc57160d5de191f4ca19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 18:05:37 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 4 +- .../domain/account/models/AccountList.kt | 30 +- .../usecase/AddCryptoPortfolioUseCase.kt | 101 +++++- .../domain/account/models/AccountListTest.kt | 284 +++++++++++++++- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 317 ++++++++++++++++++ .../tangem/domain/models/account/Account.kt | 21 ++ .../domain/models/account/AccountName.kt | 5 + .../domain/models/account/AccountNameTest.kt | 11 + .../domain/models/account/AccountTest.kt | 39 ++- 9 files changed, 781 insertions(+), 31 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 5c303041a0..c4f9660136 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -17,8 +17,8 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideAddCryptoPortfolioUseCase(): AddCryptoPortfolioUseCase { - return AddCryptoPortfolioUseCase() + fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase { + return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index e782ece215..670a7b2bb6 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -28,6 +28,10 @@ data class AccountList private constructor( val mainAccount: Account.CryptoPortfolio get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + /** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */ + val canAddMoreAccounts: Boolean + get() = accounts.size < MAX_ACCOUNTS_COUNT + /** * Adds an account to the account list. * If an account with the same identifier already exists, it will be replaced. @@ -55,12 +59,15 @@ data class AccountList private constructor( * @param other the account to remove */ operator fun minus(other: Account): Either { + val isExistingAccount = this.accounts.any { it.accountId == other.accountId } + val accounts = this.accounts.toMutableSet().apply { + removeIf { it.accountId == other.accountId } + } + return invoke( userWallet = this.userWallet, - accounts = this.accounts.toMutableSet().apply { - removeIf { it.accountId == other.accountId } - }, - totalAccounts = this.totalAccounts - 1, + accounts = accounts, + totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, ) } @@ -140,6 +147,21 @@ data class AccountList private constructor( AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) } + /** + * Factory method to create an empty [AccountList] with a main crypto portfolio account + * + * @param userWallet the user wallet associated with the account list + */ + fun createEmpty(userWallet: UserWallet): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf( + Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + ), + totalAccounts = 1, + ) + } + private fun Set.mainAccountsCount(): Int { return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true } } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index fb2ebb4717..4e68e70e3a 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -1,9 +1,13 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.Option +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.account.models.AccountList -import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase.Error.AccountCreation +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 @@ -14,19 +18,56 @@ import com.tangem.domain.models.wallet.UserWalletId import java.util.UUID /** + * Use case for adding a new crypto portfolio account + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class AddCryptoPortfolioUseCase { +class AddCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** + * Adds a new crypto portfolio account to the repository + * + * @param userWalletId the unique identifier of the user wallet + * @param accountName the name of the new account + * @param icon the icon representing the new account + * @param derivationIndex the derivation index for the account + * + + */ suspend operator fun invoke( userWalletId: UserWalletId, accountName: AccountName, icon: CryptoPortfolioIcon, derivationIndex: Int, ): Either = either { - Account.CryptoPortfolio( + val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) + + val accountList = getAccountList(userWalletId = userWalletId).getOrElse { + createNewAccountList(userWalletId = userWalletId) + } + + val updatedAccounts = (accountList + newAccount) + .getOrElse { raise(Error.AccountListRequirementsNotMet(it)) } + + saveAccounts(updatedAccounts) + + newAccount + } + + private fun Raise.createAccount( + userWalletId: UserWalletId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: Int, + ): Account.CryptoPortfolio { + // TODO: [REDACTED_JIRA] + return Account.CryptoPortfolio( accountId = AccountId(userWalletId = userWalletId, value = UUID.randomUUID().toString()), - name = accountName.value, + accountName = accountName, accountIcon = icon, derivationIndex = derivationIndex, isArchived = false, @@ -36,20 +77,56 @@ class AddCryptoPortfolioUseCase { groupType = TokensGroupType.NONE, ), ) - .mapLeft(::AccountCreation) - .bind() - - // TODO: [REDACTED_JIRA] - // Save to local store - // Save to backend (tokens migration) – asynchronously + .getOrElse { raise(Error.AccountCreation(it)) } } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Raise.createNewAccountList(userWalletId: UserWalletId): AccountList { + val userWallet = catch( + block = { crudRepository.getUserWallet(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + + return AccountList.createEmpty(userWallet = userWallet) + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the add operation + */ 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 - data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "Account list requirements not met: $cause" + } - data object DataOperationFailed : Error + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 04bb22f7b6..ca1a2d397b 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -4,11 +4,14 @@ 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.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 io.mockk.clearMocks -import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -16,6 +19,7 @@ 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] @@ -43,6 +47,44 @@ class AccountListTest { Truth.assertThat(actual).isEqualTo(expected) } + @Test + fun canAddMoreAccounts() { + // Arrange + val accountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(count = 2), + totalAccounts = 2, + ).getOrNull()!! + + val fullAccountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(20), + totalAccounts = 20, + ).getOrNull()!! + + // Act & Assert + Truth.assertThat(accountList.canAddMoreAccounts).isTrue() + Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse() + } + + @Test + fun createEmpty() { + // Arrange + val userWallet = mockk(relaxed = true) + + // Act + val actual = AccountList.createEmpty(userWallet) + + // Assert + val expected = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ).getOrNull()!! + + Truth.assertThat(actual).isEqualTo(expected) + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Create { @@ -129,6 +171,224 @@ class AccountListTest { val expected: Either, ) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Plus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: PlusTestModel) { + // Act + val actual = model.initial.plus(other = model.toAdd) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + 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) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, newAccount), + totalAccounts = 2, + ), + ) + }, + // endregion + // region Replace existing account + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + + val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(newAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = createAccounts(20), + totalAccounts = 20, + ).getOrNull()!!, + toAdd = createAccount(isMain = false), + 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(), + ), + ) + } + + data class PlusTestModel( + val initial: AccountList, + val toAdd: Account, + val expected: Either, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Minus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: MinusTestModel) { + // Act + val actual = model.initial.minus(model.toRemove) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + 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, + ) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = secondaryAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // 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, + ) + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = notInList, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + // region EmptyAccountsList + run { + val mainAccount = createAccount(isMain = true) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.EmptyAccountsList.left(), + ) + }, + // 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, + ) + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.MainAccountNotFound.left(), + ) + }, + // endregion + ) + } + + data class MinusTestModel( + val initial: AccountList, + val toRemove: Account, + val expected: Either, + ) + private fun createAccounts(count: Int): Set { return buildSet { add(createAccount(isMain = true)) @@ -139,12 +399,22 @@ class AccountListTest { } private fun createAccount( - accountId: AccountId = AccountId(value = randomAccountId(5), userWalletId = mockk()), - isMain: Boolean = false, + accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), + accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + isMain: Boolean, ): Account.CryptoPortfolio { - return mockk { - every { this@mockk.accountId } returns accountId - every { this@mockk.isMainAccount } returns isMain - } + 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()!! } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..9dc84f5b4d --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,317 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +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.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.* +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 { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = AddCryptoPortfolioUseCase(crudRepository) + + private val userWalletId = UserWalletId("011") + private val userWallet = mockk() + + @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 updatedAccountList = (accountList + newAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } just Runs + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + 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 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( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + 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 + fun `invoke should return error if account list requirements not met`() = runTest { + // Arrange + val accountList = AccountList( + userWallet = userWallet, + accounts = createAccounts(count = 20), + totalAccounts = 20, + ).getOrNull()!! + + val newAccount = createAccount( + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet( + cause = AccountList.Error.ExceedsMaxAccountsCount, + ).left() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val newAccount = createAccount( + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @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 updatedAccountList = (accountList + newAccount).getOrNull()!! + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + // val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + // Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + } + + private fun createAccounts(count: Int): Set { + return buildSet { + add(createAccount(derivationIndex = 0)) + repeat(count - 1) { + add(createAccount()) + } + } + } + + 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()!! + } +} \ No newline at end of file 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 index 8d7b786cca..73789d0c65 100644 --- 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 @@ -176,6 +176,27 @@ sealed interface Account { ) } } + + /** + * Creates a main account for the given user wallet ID + * + * @param userWalletId the ID of the user wallet + */ + fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + // TODO: [REDACTED_JIRA] + return CryptoPortfolio( + accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + name = AccountName.Main, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } } } } \ 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 index 532687ef6c..29fe653a8b 100644 --- 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 @@ -44,8 +44,13 @@ data class AccountName private constructor( companion object { + private const val MAIN_ACCOUNT_NAME = "Main Account" private const val MAX_LENGTH = 20 + /** Default name for the main account */ + val Main: AccountName + get() = AccountName(value = MAIN_ACCOUNT_NAME) + /** * Factory method to create an `AccountName` instance. * Validates the input string to ensure it is not blank and does not exceed the maximum length. 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 index d68bc46ab9..e2aee138be 100644 --- 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 @@ -3,6 +3,7 @@ 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.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -13,6 +14,16 @@ import org.junit.jupiter.params.provider.MethodSource @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AccountNameTest { + @Test + fun main_returnsMainAccountName() { + // Act + val main = AccountName.Main.value + + // Assert + val expected = "Main Account" + Truth.assertThat(main).isEqualTo(expected) + } + @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: InvokeTestModel) { 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 index d94c1a6bdf..dac8fdac45 100644 --- 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 @@ -3,6 +3,7 @@ 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 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 @@ -99,7 +100,7 @@ class AccountTest { val name = "" // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = mockk(), name = name, accountIcon = mockk(), @@ -120,7 +121,7 @@ class AccountTest { val derivationIndex = -1 // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = mockk(), name = "Test Account", accountIcon = mockk(), @@ -131,14 +132,14 @@ class AccountTest { .leftOrNull()!! // Assert - val expected = Account.CryptoPortfolio.Error.NegativeDerivationIndex + val expected = CryptoPortfolio.Error.NegativeDerivationIndex Truth.assertThat(actual).isEqualTo(expected) } @Test fun `invoke returns CryptoPortfolio`() { // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = AccountId( value = "value", userWalletId = UserWalletId("011"), @@ -159,6 +160,32 @@ class AccountTest { val expected = createCryptoPortfolioStub() Truth.assertThat(actual).isEqualTo(expected) } + + @Test + fun createMainAccount() { + // Arrange + val userWalletId = UserWalletId("011") + + // Act + val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + // Assert + // TODO: [REDACTED_JIRA] + val expected = CryptoPortfolio( + accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + accountName = AccountName.Main, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ).getOrNull() + + Truth.assertThat(actual).isEqualTo(expected) + } } private fun createCryptoPortfolioStub( @@ -166,8 +193,8 @@ class AccountTest { name: String = "Test Account", derivationIndex: Int = 0, currencies: Set = emptySet(), - ): Account.CryptoPortfolio { - return Account.CryptoPortfolio.invoke( + ): CryptoPortfolio { + return CryptoPortfolio.invoke( accountId = AccountId( value = "value", userWalletId = userWalletId,