Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 18:16:09 +03:00
commit daa96ef62a
79 changed files with 2943 additions and 843 deletions

View file

@ -8,10 +8,11 @@ import kotlinx.coroutines.flow.Flow
/** Persistence port for the address book. The implementation is provided by the data layer. */
interface AddressBookRepository {
/** Contacts for a single wallet. Each [Contact] keeps its own [Contact.walletId]. */
fun getContacts(userWalletId: UserWalletId): Flow<List<Contact>>
/** Contacts across several wallets, flattened. Each [Contact] keeps its own [Contact.walletId]. */
fun getContacts(userWalletIds: Set<UserWalletId>): Flow<List<Contact>>
/** Contacts across all wallets (flattened). Each [Contact] keeps its own [Contact.walletId]. */
fun getAllContacts(): Flow<List<Contact>>
suspend fun getContact(userWalletId: UserWalletId, name: String): Contact?

View file

@ -4,10 +4,29 @@ import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.repository.AddressBookRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetContactsUseCase(
private val repository: AddressBookRepository,
) {
operator fun invoke(userWalletIds: Set<UserWalletId>): Flow<List<Contact>> = repository.getContacts(userWalletIds)
operator fun invoke(query: String, userWalletId: UserWalletId? = null): Flow<List<Contact>> {
val source = if (userWalletId == null) {
repository.getAllContacts()
} else {
repository.getContacts(userWalletId)
}
val normalizedQuery = query.trim()
if (normalizedQuery.isEmpty()) return source
return source
.map { contacts ->
contacts.filter { contact ->
val isNameContaining = contact.name.value.contains(other = normalizedQuery, ignoreCase = true)
val isAddressContaining = contact.addressEntries.any { addressEntry ->
addressEntry.address.contains(other = normalizedQuery, ignoreCase = true)
}
isNameContaining || isAddressContaining
}
}
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.domain.addressbook.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.addressbook.model.AddressEntry
import com.tangem.domain.addressbook.model.AddressEntryId
import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.model.ContactId
import com.tangem.domain.addressbook.model.ContactName
import com.tangem.domain.addressbook.repository.AddressBookRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetContactsUseCaseTest {
private val repository: AddressBookRepository = mockk()
private val useCase = GetContactsUseCase(repository)
private val alice = contact(name = "Alice", address = "0xaaa")
private val bob = contact(name = "Bob", address = "0xbbb")
@BeforeEach
fun resetMocks() {
clearMocks(repository)
every { repository.getAllContacts() } returns flowOf(listOf(alice, bob))
}
@Test
fun `GIVEN query matches a name WHEN invoke THEN returns only matching contacts`() = runTest {
// Act
val result = useCase(query = "ali").first()
// Assert
assertThat(result).containsExactly(alice)
}
@Test
fun `GIVEN query matches an address WHEN invoke THEN returns only matching contacts`() = runTest {
// Act
val result = useCase(query = "0xbbb").first()
// Assert
assertThat(result).containsExactly(bob)
}
@Test
fun `GIVEN blank query WHEN invoke THEN returns all contacts unfiltered`() = runTest {
// Act
val result = useCase(query = " ").first()
// Assert
assertThat(result).containsExactly(alice, bob)
}
@Test
fun `GIVEN query matches nothing WHEN invoke THEN returns empty list`() = runTest {
// Act
val result = useCase(query = "charlie").first()
// Assert
assertThat(result).isEmpty()
}
@Test
fun `GIVEN userWalletId WHEN invoke THEN reads single wallet contacts AND not all contacts`() = runTest {
// Arrange
val walletId = UserWalletId("011")
every { repository.getContacts(walletId) } returns flowOf(listOf(alice))
// Act
val result = useCase(query = "", userWalletId = walletId).first()
// Assert
assertThat(result).containsExactly(alice)
verify(exactly = 1) { repository.getContacts(walletId) }
verify(exactly = 0) { repository.getAllContacts() }
}
private fun contact(name: String, address: String): Contact = Contact(
id = ContactId("id-$name"),
walletId = UserWalletId("011"),
name = requireNotNull(ContactName(name).getOrNull()),
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = address,
networkId = Network.RawID("ethereum"),
memo = null,
signature = "sig",
),
),
)
}