BIT-1057: Vault item listing functionality (#379)

This commit is contained in:
Ramsey Smith 2023-12-13 14:31:15 -07:00 committed by Álison Fernandes
parent 65a9f209c2
commit 5c7d0081a3
6 changed files with 967 additions and 31 deletions

View file

@ -4,14 +4,21 @@ import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@ -19,9 +26,10 @@ import javax.inject.Inject
* and launches [VaultItemListingEvent] for the [VaultItemListingScreen].
*/
@HiltViewModel
@Suppress("MagicNumber")
@Suppress("MagicNumber", "TooManyFunctions")
class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
@ -32,15 +40,10 @@ class VaultItemListingViewModel @Inject constructor(
) {
init {
// TODO fetch real listing data in BIT-1057
viewModelScope.launch {
delay(2000)
mutableStateFlow.update {
it.copy(
viewState = VaultItemListingState.ViewState.NoItems,
)
}
}
vaultRepository
.vaultDataStateFlow
.onEach { sendAction(VaultItemListingsAction.Internal.VaultDataReceive(it)) }
.launchIn(viewModelScope)
}
override fun handleAction(action: VaultItemListingsAction) {
@ -50,17 +53,13 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.ItemClick -> handleItemClick(action)
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
is VaultItemListingsAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
}
}
//region VaultItemListing Handlers
private fun handleRefreshClick() {
// TODO implement refresh in BIT-1057
sendEvent(
event = VaultItemListingEvent.ShowToast(
text = "Not yet implemented".asText(),
),
)
vaultRepository.sync()
}
private fun handleAddVaultItemClick() {
@ -88,7 +87,80 @@ class VaultItemListingViewModel @Inject constructor(
event = VaultItemListingEvent.NavigateToVaultSearchScreen,
)
}
private fun handleVaultDataReceive(
action: VaultItemListingsAction.Internal.VaultDataReceive,
) {
when (val vaultData = action.vaultData) {
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
is DataState.Loading -> vaultLoadingReceive()
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
}
}
//endregion VaultItemListing Handlers
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
if (vaultData.data != null) {
updateStateWithVaultData(vaultData = vaultData.data)
} else {
mutableStateFlow.update {
it.copy(
viewState = VaultItemListingState.ViewState.Error(
message = R.string.generic_error_message.asText(),
),
)
}
}
}
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data)
}
private fun vaultLoadingReceive() {
mutableStateFlow.update { it.copy(viewState = VaultItemListingState.ViewState.Loading) }
}
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
if (vaultData.data != null) {
updateStateWithVaultData(vaultData = vaultData.data)
} else {
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = VaultItemListingState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText()),
),
)
}
}
}
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data)
}
private fun updateStateWithVaultData(vaultData: VaultData) {
mutableStateFlow.update { currentState ->
currentState.copy(
itemListingType = currentState
.itemListingType
.updateWithAdditionalDataIfNecessary(
folderList = vaultData
.folderViewList,
),
viewState = vaultData
.cipherViewList
.filter { cipherView ->
cipherView.determineListingPredicate(currentState.itemListingType)
}
.toViewState(),
)
}
}
}
/**
@ -139,14 +211,14 @@ data class VaultItemListingState(
*
* @property id the id of the item.
* @property title title of the item.
* @property subtitle subtitle of the item.
* @property subtitle subtitle of the item (nullable).
* @property uri uri for the icon to be displayed (nullable).
* @property iconRes the icon to be displayed.
*/
data class DisplayItem(
val id: String,
val title: String,
val subtitle: String,
val subtitle: String?,
val uri: String?,
@DrawableRes
val iconRes: Int,
@ -302,4 +374,17 @@ sealed class VaultItemListingsAction {
* @property id the id of the item that has been clicked.
*/
data class ItemClick(val id: String) : VaultItemListingsAction()
/**
* Models actions that the [VaultItemListingViewModel] itself might send.
*/
sealed class Internal : VaultItemListingsAction() {
/**
* Indicates vault data was received.
*/
data class VaultDataReceive(
val vaultData: DataState<VaultData>,
) : Internal()
}
}

View file

@ -0,0 +1,123 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import androidx.annotation.DrawableRes
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.FolderView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
/**
* Determines a predicate to filter a list of [CipherView] based on the
* [VaultItemListingState.ItemListingType].
*/
fun CipherView.determineListingPredicate(
itemListingType: VaultItemListingState.ItemListingType,
): Boolean =
when (itemListingType) {
is VaultItemListingState.ItemListingType.Card -> {
type == CipherType.CARD && deletedDate == null
}
is VaultItemListingState.ItemListingType.Folder -> {
folderId == itemListingType.folderId && deletedDate == null
}
is VaultItemListingState.ItemListingType.Identity -> {
type == CipherType.IDENTITY && deletedDate == null
}
is VaultItemListingState.ItemListingType.Login -> {
type == CipherType.LOGIN && deletedDate == null
}
is VaultItemListingState.ItemListingType.SecureNote -> {
type == CipherType.SECURE_NOTE && deletedDate == null
}
is VaultItemListingState.ItemListingType.Trash -> {
deletedDate != null
}
}
/**
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
*/
fun List<CipherView>.toViewState(): VaultItemListingState.ViewState =
if (isNotEmpty()) {
VaultItemListingState.ViewState.Content(displayItemList = toDisplayItemList())
} else {
VaultItemListingState.ViewState.NoItems
}
/** * Updates a [VaultItemListingState.ItemListingType] with the given data if necessary. */
fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
folderList: List<FolderView>,
): VaultItemListingState.ItemListingType =
when (this) {
is VaultItemListingState.ItemListingType.Card -> this
is VaultItemListingState.ItemListingType.Folder -> copy(
folderName = folderList.first { it.id == folderId }.name,
)
is VaultItemListingState.ItemListingType.Identity -> this
is VaultItemListingState.ItemListingType.Login -> this
is VaultItemListingState.ItemListingType.SecureNote -> this
is VaultItemListingState.ItemListingType.Trash -> this
}
private fun List<CipherView>.toDisplayItemList(): List<VaultItemListingState.DisplayItem> =
this.map { it.toDisplayItem() }
private fun CipherView.toDisplayItem(): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem(
id = id.orEmpty(),
title = name,
subtitle = subtitle,
iconRes = type.iconRes,
uri = uri,
)
@Suppress("MagicNumber")
private val CipherView.subtitle: String?
get() = when (type) {
CipherType.LOGIN -> login?.username.orEmpty()
CipherType.SECURE_NOTE -> null
CipherType.CARD -> {
card
?.number
?.takeLast(4)
.orEmpty()
}
CipherType.IDENTITY -> {
identity
?.firstName
.orEmpty()
.plus(identity?.lastName.orEmpty())
}
}
@get:DrawableRes
private val CipherType.iconRes: Int
get() = when (this) {
CipherType.LOGIN -> R.drawable.ic_login_item
CipherType.SECURE_NOTE -> R.drawable.ic_secure_note_item
CipherType.CARD -> R.drawable.ic_card_item
CipherType.IDENTITY -> R.drawable.ic_identity_item
}
private val CipherView.uri: String?
get() = when (type) {
CipherType.LOGIN -> {
login
?.uris
?.firstOrNull()
?.uri
.orEmpty()
}
CipherType.SECURE_NOTE -> null
CipherType.CARD -> null
CipherType.IDENTITY -> null
}

View file

@ -18,9 +18,17 @@ import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Create a mock [CipherView] with a given [number].
* Create a mock [CipherView].
*
* @param number the number to create the cipher with.
* @param isDeleted whether or not the cipher has been deleted.
* @param cipherType the type of cipher to create.
*/
fun createMockCipherView(number: Int): CipherView =
fun createMockCipherView(
number: Int,
isDeleted: Boolean = true,
cipherType: CipherType = CipherType.LOGIN,
): CipherView =
CipherView(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
@ -29,14 +37,18 @@ fun createMockCipherView(number: Int): CipherView =
key = "mockKey-$number",
name = "mockName-$number",
notes = "mockNotes-$number",
type = CipherType.LOGIN,
type = cipherType,
login = createMockLoginView(number = number),
creationDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
deletedDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
deletedDate = if (isDeleted) {
LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC)
} else {
null
},
revisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),

View file

@ -2,15 +2,33 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockItemListingDisplayItem
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultItemListingViewModelTest : BaseViewModelTest() {
private val mutableVaultDataStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
private val vaultRepository: VaultRepository = mockk {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
every { sync() } returns Unit
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@ -63,15 +81,330 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
@Test
fun `RefreshClick should emit ShowToast`() = runTest {
fun `RefreshClick should sync`() = runTest {
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultItemListingsAction.RefreshClick)
viewModel.actionChannel.trySend(VaultItemListingsAction.RefreshClick)
verify { vaultRepository.sync() }
}
@Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = listOf(
createMockCipherView(
number = 1,
isDeleted = false,
),
),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingEvent.ShowToast("Not yet implemented".asText()),
awaitItem(),
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content(
displayItemList = listOf(
createMockItemListingDisplayItem(number = 1),
),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loaded with empty items should update ViewState to NoItems`() =
runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = emptyList(),
folderViewList = emptyList(),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(viewState = VaultItemListingState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loaded with trash items should update ViewState to NoItems`() =
runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems,
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(viewState = VaultItemListingState.ViewState.Loading),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content(
displayItemList = listOf(
createMockItemListingDisplayItem(number = 1),
),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Pending with empty data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(viewState = VaultItemListingState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Pending with trash data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(viewState = VaultItemListingState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Error without data should update state to Error`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
error = IllegalStateException(),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Error(
message = R.string.generic_error_message.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
error = IllegalStateException(),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content(
displayItemList = listOf(
createMockItemListingDisplayItem(number = 1),
),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Error with empty data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
data = VaultData(
cipherViewList = emptyList(),
folderViewList = emptyList(),
),
error = IllegalStateException(),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems,
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Error with trash data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
error = IllegalStateException(),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems,
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow NoNetwork without data should update state to Error`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText()),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
folderViewList = listOf(createMockFolderView(number = 1)),
)),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content(
displayItemList = listOf(
createMockItemListingDisplayItem(number = 1),
),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow NoNetwork with empty data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
cipherViewList = emptyList(),
folderViewList = emptyList(),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems,
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow NoNetwork with trash data should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems,
),
viewModel.stateFlow.value,
)
}
private fun createSavedStateHandleWithVaultItemListingType(
@ -103,9 +436,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
private fun createVaultItemListingViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
vaultRepository: VaultRepository = this.vaultRepository,
): VaultItemListingViewModel =
VaultItemListingViewModel(
savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository,
)
@Suppress("MaxLineLength")

View file

@ -0,0 +1,327 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import org.junit.Assert.assertEquals
import org.junit.Test
class VaultItemListingDataExtensionsTest {
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for non trash Login cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.LOGIN,
)
mapOf(
VaultItemListingState.ItemListingType.Login to true,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for trash Login cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.LOGIN,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for non trash Card cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.CARD,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to true,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for trash Card cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.CARD,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for non trash Identity cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.IDENTITY,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to true,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for trash Identity cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.IDENTITY,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for non trash SecureNote cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.SECURE_NOTE,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to true,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for trash SecureNote cipherView`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = true,
cipherType = CipherType.SECURE_NOTE,
)
mapOf(
VaultItemListingState.ItemListingType.Login to false,
VaultItemListingState.ItemListingType.Card to false,
VaultItemListingState.ItemListingType.SecureNote to false,
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
fun `toViewState should transform a list of CipherViews into a ViewState`() {
val cipherViewList = listOf(
createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.LOGIN,
),
createMockCipherView(
number = 2,
isDeleted = false,
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 3,
isDeleted = false,
cipherType = CipherType.SECURE_NOTE,
),
createMockCipherView(
number = 4,
isDeleted = false,
cipherType = CipherType.IDENTITY,
),
)
val result = cipherViewList.toViewState()
assertEquals(
VaultItemListingState.ViewState.Content(
displayItemList = listOf(
createMockItemListingDisplayItem(
number = 1,
cipherType = CipherType.LOGIN,
),
createMockItemListingDisplayItem(
number = 2,
cipherType = CipherType.CARD,
),
createMockItemListingDisplayItem(
number = 3,
cipherType = CipherType.SECURE_NOTE,
),
createMockItemListingDisplayItem(
number = 4,
cipherType = CipherType.IDENTITY,
),
),
),
result,
)
}
@Test
fun `updateWithAdditionalDataIfNecessary should update a folder itemListingType`() {
val folderViewList = listOf(
createMockFolderView(number = 1),
createMockFolderView(number = 2),
createMockFolderView(number = 3),
)
val result = VaultItemListingState.ItemListingType.Folder(
folderId = "mockId-1",
folderName = "wrong name",
)
.updateWithAdditionalDataIfNecessary(folderList = folderViewList)
assertEquals(
VaultItemListingState.ItemListingType.Folder(
folderId = "mockId-1",
folderName = "mockName-1",
),
result,
)
}
@Test
fun `updateWithAdditionalDataIfNecessary should not change a non folder itemListingType`() {
val folderViewList = listOf(
createMockFolderView(number = 1),
createMockFolderView(number = 2),
createMockFolderView(number = 3),
)
val result = VaultItemListingState.ItemListingType.Login
.updateWithAdditionalDataIfNecessary(folderList = folderViewList)
assertEquals(
VaultItemListingState.ItemListingType.Login,
result,
)
}
}

View file

@ -0,0 +1,54 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
/**
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
*/
fun createMockItemListingDisplayItem(
number: Int,
cipherType: CipherType = CipherType.LOGIN,
): VaultItemListingState.DisplayItem =
when (cipherType) {
CipherType.LOGIN -> {
VaultItemListingState.DisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockUsername-$number",
iconRes = R.drawable.ic_login_item,
uri = "mockUri-$number",
)
}
CipherType.SECURE_NOTE -> {
VaultItemListingState.DisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = null,
iconRes = R.drawable.ic_secure_note_item,
uri = null,
)
}
CipherType.CARD -> {
VaultItemListingState.DisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "er-$number",
iconRes = R.drawable.ic_card_item,
uri = null,
)
}
CipherType.IDENTITY -> {
VaultItemListingState.DisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number",
iconRes = R.drawable.ic_identity_item,
uri = null,
)
}
}