mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
BIT-617: Vault Password History (#935)
This commit is contained in:
parent
46bc489f1f
commit
8156e306f5
12 changed files with 427 additions and 74 deletions
|
@ -22,6 +22,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolder
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorModalDestination
|
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorModalDestination
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorModal
|
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorModal
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination
|
||||||
|
@ -90,7 +91,11 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
|
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
|
||||||
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
||||||
onNavigateToPendingRequests = { navController.navigateToPendingRequests() },
|
onNavigateToPendingRequests = { navController.navigateToPendingRequests() },
|
||||||
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
|
onNavigateToPasswordHistory = {
|
||||||
|
navController.navigateToPasswordHistory(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Default,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
loginApprovalDestination(onNavigateBack = { navController.popBackStack() })
|
loginApprovalDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
|
@ -136,6 +141,11 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNavigateToAttachments = { navController.navigateToAttachment(it) },
|
onNavigateToAttachments = { navController.navigateToAttachment(it) },
|
||||||
|
onNavigateToPasswordHistory = {
|
||||||
|
navController.navigateToPasswordHistory(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = it),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
vaultQrCodeScanDestination(
|
vaultQrCodeScanDestination(
|
||||||
onNavigateToManualCodeEntryScreen = {
|
onNavigateToManualCodeEntryScreen = {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.x8bit.bitwarden.ui.tools.feature.generator.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the different modes the password history screen can be in.
|
||||||
|
*/
|
||||||
|
sealed class GeneratorPasswordHistoryMode : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the main or default password history mode.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Default : GeneratorPasswordHistoryMode()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the item password history mode.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Item(val itemId: String) : GeneratorPasswordHistoryMode()
|
||||||
|
}
|
|
@ -1,14 +1,45 @@
|
||||||
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
|
||||||
|
|
||||||
|
private const val DEFAULT_MODE: String = "default"
|
||||||
|
private const val ITEM_MODE: String = "item"
|
||||||
|
|
||||||
|
private const val PASSWORD_HISTORY_PREFIX: String = "password_history"
|
||||||
|
private const val PASSWORD_HISTORY_MODE: String = "password_history_mode"
|
||||||
|
private const val PASSWORD_HISTORY_ITEM_ID: String = "password_history_id"
|
||||||
|
|
||||||
|
private const val PASSWORD_HISTORY_ROUTE: String =
|
||||||
|
PASSWORD_HISTORY_PREFIX +
|
||||||
|
"/{$PASSWORD_HISTORY_MODE}" +
|
||||||
|
"?$PASSWORD_HISTORY_ITEM_ID={$PASSWORD_HISTORY_ITEM_ID}"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The functions below pertain to entry into the [PasswordHistoryScreen].
|
* Class to retrieve password history arguments from the [SavedStateHandle].
|
||||||
*/
|
*/
|
||||||
private const val PASSWORD_HISTORY_ROUTE: String = "password_history"
|
@OmitFromCoverage
|
||||||
|
data class PasswordHistoryArgs(
|
||||||
|
val passwordHistoryMode: GeneratorPasswordHistoryMode,
|
||||||
|
) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||||
|
passwordHistoryMode = when (requireNotNull(savedStateHandle[PASSWORD_HISTORY_MODE])) {
|
||||||
|
DEFAULT_MODE -> GeneratorPasswordHistoryMode.Default
|
||||||
|
ITEM_MODE -> GeneratorPasswordHistoryMode.Item(
|
||||||
|
requireNotNull(savedStateHandle[PASSWORD_HISTORY_ITEM_ID]),
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> throw IllegalStateException("Unknown VaultAddEditType.")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add password history destination to the graph.
|
* Add password history destination to the graph.
|
||||||
|
@ -17,8 +48,10 @@ fun NavGraphBuilder.passwordHistoryDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
// TODO: (BIT-617) Allow Password History screen to launch from VaultItemScreen
|
|
||||||
route = PASSWORD_HISTORY_ROUTE,
|
route = PASSWORD_HISTORY_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(PASSWORD_HISTORY_MODE) { type = NavType.StringType },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
PasswordHistoryScreen(
|
PasswordHistoryScreen(
|
||||||
onNavigateBack = onNavigateBack,
|
onNavigateBack = onNavigateBack,
|
||||||
|
@ -29,6 +62,25 @@ fun NavGraphBuilder.passwordHistoryDestination(
|
||||||
/**
|
/**
|
||||||
* Navigate to the Password History Screen.
|
* Navigate to the Password History Screen.
|
||||||
*/
|
*/
|
||||||
fun NavController.navigateToPasswordHistory(navOptions: NavOptions? = null) {
|
fun NavController.navigateToPasswordHistory(
|
||||||
navigate(PASSWORD_HISTORY_ROUTE, navOptions)
|
passwordHistoryMode: GeneratorPasswordHistoryMode,
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
navigate(
|
||||||
|
route = "$PASSWORD_HISTORY_PREFIX/${passwordHistoryMode.toModeString()}" +
|
||||||
|
"?$PASSWORD_HISTORY_ITEM_ID=${passwordHistoryMode.toIdOrNull()}",
|
||||||
|
navOptions = navOptions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun GeneratorPasswordHistoryMode.toModeString(): String =
|
||||||
|
when (this) {
|
||||||
|
is GeneratorPasswordHistoryMode.Default -> DEFAULT_MODE
|
||||||
|
is GeneratorPasswordHistoryMode.Item -> ITEM_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GeneratorPasswordHistoryMode.toIdOrNull(): String? =
|
||||||
|
when (this) {
|
||||||
|
is GeneratorPasswordHistoryMode.Default -> null
|
||||||
|
is GeneratorPasswordHistoryMode.Item -> itemId
|
||||||
|
}
|
||||||
|
|
|
@ -82,21 +82,23 @@ fun PasswordHistoryScreen(
|
||||||
{ viewModel.trySendAction(PasswordHistoryAction.CloseClick) }
|
{ viewModel.trySendAction(PasswordHistoryAction.CloseClick) }
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
BitwardenOverflowActionItem(
|
if (state.menuEnabled) {
|
||||||
menuItemDataList = persistentListOf(
|
BitwardenOverflowActionItem(
|
||||||
OverflowMenuItemData(
|
menuItemDataList = persistentListOf(
|
||||||
testTag = "ClearPasswordList",
|
OverflowMenuItemData(
|
||||||
text = stringResource(id = R.string.clear),
|
testTag = "ClearPasswordList",
|
||||||
onClick = remember(viewModel) {
|
text = stringResource(id = R.string.clear),
|
||||||
{
|
onClick = remember(viewModel) {
|
||||||
viewModel.trySendAction(
|
{
|
||||||
PasswordHistoryAction.PasswordClearClick,
|
viewModel.trySendAction(
|
||||||
)
|
PasswordHistoryAction.PasswordClearClick,
|
||||||
}
|
)
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
import com.bitwarden.core.PasswordHistoryView
|
import com.bitwarden.core.PasswordHistoryView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
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.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.PasswordHistoryState.GeneratedPassword
|
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.PasswordHistoryState.GeneratedPassword
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
@ -21,24 +26,45 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel responsible for handling user interactions in the PasswordHistoryScreen.
|
* ViewModel responsible for handling user interactions in the PasswordHistoryScreen.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class PasswordHistoryViewModel @Inject constructor(
|
class PasswordHistoryViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
private val clipboardManager: BitwardenClipboardManager,
|
private val clipboardManager: BitwardenClipboardManager,
|
||||||
private val generatorRepository: GeneratorRepository,
|
private val generatorRepository: GeneratorRepository,
|
||||||
|
private val vaultRepository: VaultRepository,
|
||||||
) : BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
|
) : BaseViewModel<PasswordHistoryState, PasswordHistoryEvent, PasswordHistoryAction>(
|
||||||
initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: run {
|
||||||
|
PasswordHistoryState(
|
||||||
|
passwordHistoryMode = PasswordHistoryArgs(savedStateHandle).passwordHistoryMode,
|
||||||
|
viewState = PasswordHistoryState.ViewState.Loading,
|
||||||
|
)
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
generatorRepository
|
when (val passwordHistoryMode = state.passwordHistoryMode) {
|
||||||
.passwordHistoryStateFlow
|
is GeneratorPasswordHistoryMode.Default -> {
|
||||||
.map { PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive(it) }
|
generatorRepository
|
||||||
.onEach(::sendAction)
|
.passwordHistoryStateFlow
|
||||||
.launchIn(viewModelScope)
|
.map { PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive(it) }
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
is GeneratorPasswordHistoryMode.Item -> {
|
||||||
|
vaultRepository
|
||||||
|
.getVaultItemStateFlow(passwordHistoryMode.itemId)
|
||||||
|
.map { PasswordHistoryAction.Internal.CipherDataReceive(it) }
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleAction(action: PasswordHistoryAction) {
|
override fun handleAction(action: PasswordHistoryAction) {
|
||||||
|
@ -49,6 +75,8 @@ class PasswordHistoryViewModel @Inject constructor(
|
||||||
is PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive -> {
|
is PasswordHistoryAction.Internal.UpdatePasswordHistoryReceive -> {
|
||||||
handleUpdatePasswordHistoryReceive(action)
|
handleUpdatePasswordHistoryReceive(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is PasswordHistoryAction.Internal.CipherDataReceive -> handleCipherDataReceive(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,22 +90,7 @@ class PasswordHistoryViewModel @Inject constructor(
|
||||||
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText())
|
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText())
|
||||||
}
|
}
|
||||||
|
|
||||||
is LocalDataState.Loaded -> {
|
is LocalDataState.Loaded -> state.data.toViewState()
|
||||||
val passwords = state.data.map { passwordHistoryView ->
|
|
||||||
GeneratedPassword(
|
|
||||||
password = passwordHistoryView.password,
|
|
||||||
date = passwordHistoryView.lastUsedDate.toFormattedPattern(
|
|
||||||
pattern = "MM/dd/yy h:mm a",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passwords.isEmpty()) {
|
|
||||||
PasswordHistoryState.ViewState.Empty
|
|
||||||
} else {
|
|
||||||
PasswordHistoryState.ViewState.Content(passwords)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
|
@ -85,6 +98,21 @@ class PasswordHistoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCipherDataReceive(action: PasswordHistoryAction.Internal.CipherDataReceive) {
|
||||||
|
val newState: PasswordHistoryState.ViewState = when (action.state) {
|
||||||
|
is DataState.Error -> {
|
||||||
|
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText())
|
||||||
|
}
|
||||||
|
is DataState.Loaded -> action.state.data?.passwordHistory.toViewState()
|
||||||
|
is DataState.Loading -> PasswordHistoryState.ViewState.Loading
|
||||||
|
is DataState.NoNetwork -> {
|
||||||
|
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText())
|
||||||
|
}
|
||||||
|
is DataState.Pending -> action.state.data?.passwordHistory.toViewState()
|
||||||
|
}
|
||||||
|
mutableStateFlow.update { it.copy(viewState = newState) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCloseClick() {
|
private fun handleCloseClick() {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
event = PasswordHistoryEvent.NavigateBack,
|
event = PasswordHistoryEvent.NavigateBack,
|
||||||
|
@ -100,18 +128,41 @@ class PasswordHistoryViewModel @Inject constructor(
|
||||||
private fun handleCopyClick(password: GeneratedPassword) {
|
private fun handleCopyClick(password: GeneratedPassword) {
|
||||||
clipboardManager.setText(text = password.password)
|
clipboardManager.setText(text = password.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<PasswordHistoryView>?.toViewState(): PasswordHistoryState.ViewState {
|
||||||
|
val passwords = this?.map { passwordHistoryView ->
|
||||||
|
GeneratedPassword(
|
||||||
|
password = passwordHistoryView.password,
|
||||||
|
date = passwordHistoryView.lastUsedDate.toFormattedPattern(
|
||||||
|
pattern = "MM/dd/yy h:mm a",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return if (passwords?.isNotEmpty() == true) {
|
||||||
|
PasswordHistoryState.ViewState.Content(passwords)
|
||||||
|
} else {
|
||||||
|
PasswordHistoryState.ViewState.Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the possible states for the password history screen.
|
* Represents the possible states for the password history screen.
|
||||||
*
|
*
|
||||||
|
* @property passwordHistoryMode Indicates whether tje VM is in default or item mode.
|
||||||
* @property viewState The current view state of the password history screen.
|
* @property viewState The current view state of the password history screen.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PasswordHistoryState(
|
data class PasswordHistoryState(
|
||||||
|
val passwordHistoryMode: GeneratorPasswordHistoryMode,
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that represents if the menu is enabled.
|
||||||
|
*/
|
||||||
|
val menuEnabled: Boolean
|
||||||
|
get() = passwordHistoryMode is GeneratorPasswordHistoryMode.Default
|
||||||
/**
|
/**
|
||||||
* Represents the specific view states for the password history screen.
|
* Represents the specific view states for the password history screen.
|
||||||
*/
|
*/
|
||||||
|
@ -211,5 +262,12 @@ sealed class PasswordHistoryAction {
|
||||||
data class UpdatePasswordHistoryReceive(
|
data class UpdatePasswordHistoryReceive(
|
||||||
val state: LocalDataState<List<PasswordHistoryView>>,
|
val state: LocalDataState<List<PasswordHistoryView>>,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates cipher data is received.
|
||||||
|
*/
|
||||||
|
data class CipherDataReceive(
|
||||||
|
val state: DataState<CipherView?>,
|
||||||
|
) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
|
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = VAULT_ITEM_ROUTE,
|
route = VAULT_ITEM_ROUTE,
|
||||||
|
@ -43,6 +44,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||||
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
|
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
|
||||||
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
|
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
|
||||||
onNavigateToAttachments = onNavigateToAttachments,
|
onNavigateToAttachments = onNavigateToAttachments,
|
||||||
|
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ fun VaultItemScreen(
|
||||||
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
|
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -92,8 +93,7 @@ fun VaultItemScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToPasswordHistory -> {
|
is VaultItemEvent.NavigateToPasswordHistory -> {
|
||||||
// TODO Implement password history in BIT-617
|
onNavigateToPasswordHistory(event.itemId)
|
||||||
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
|
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.bitwarden.core.FieldView
|
||||||
import com.bitwarden.core.IdentityView
|
import com.bitwarden.core.IdentityView
|
||||||
import com.bitwarden.core.LoginUriView
|
import com.bitwarden.core.LoginUriView
|
||||||
import com.bitwarden.core.LoginView
|
import com.bitwarden.core.LoginView
|
||||||
|
import com.bitwarden.core.PasswordHistoryView
|
||||||
import com.bitwarden.core.SecureNoteType
|
import com.bitwarden.core.SecureNoteType
|
||||||
import com.bitwarden.core.SecureNoteView
|
import com.bitwarden.core.SecureNoteView
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||||
|
@ -33,7 +34,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
|
||||||
localData = common.originalCipher?.localData,
|
localData = common.originalCipher?.localData,
|
||||||
attachments = common.originalCipher?.attachments,
|
attachments = common.originalCipher?.attachments,
|
||||||
organizationUseTotp = common.originalCipher?.organizationUseTotp ?: false,
|
organizationUseTotp = common.originalCipher?.organizationUseTotp ?: false,
|
||||||
passwordHistory = common.originalCipher?.passwordHistory,
|
passwordHistory = toPasswordHistory(),
|
||||||
creationDate = common.originalCipher?.creationDate ?: Instant.now(),
|
creationDate = common.originalCipher?.creationDate ?: Instant.now(),
|
||||||
deletedDate = common.originalCipher?.deletedDate,
|
deletedDate = common.originalCipher?.deletedDate,
|
||||||
revisionDate = common.originalCipher?.revisionDate ?: Instant.now(),
|
revisionDate = common.originalCipher?.revisionDate ?: Instant.now(),
|
||||||
|
@ -114,6 +115,26 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toIdentityView(): Ident
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun VaultAddEditState.ViewState.Content.toPasswordHistory(): List<PasswordHistoryView>? {
|
||||||
|
val oldPassword = common.originalCipher?.login?.password
|
||||||
|
|
||||||
|
return if (oldPassword != null &&
|
||||||
|
oldPassword != (type as? VaultAddEditState.ViewState.Content.ItemType.Login)?.password
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
PasswordHistoryView(
|
||||||
|
password = oldPassword,
|
||||||
|
lastUsedDate = Instant.now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.plus(common.originalCipher?.passwordHistory.orEmpty())
|
||||||
|
.take(5)
|
||||||
|
} else {
|
||||||
|
common.originalCipher?.passwordHistory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView(
|
private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView(
|
||||||
common: VaultAddEditState.ViewState.Content.Common,
|
common: VaultAddEditState.ViewState.Content.Common,
|
||||||
): LoginView? =
|
): LoginView? =
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
|
import androidx.compose.ui.test.filterToOne
|
||||||
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
|
import androidx.compose.ui.test.isPopup
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
@ -21,7 +28,10 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<PasswordHistoryEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<PasswordHistoryEvent>()
|
||||||
|
|
||||||
private val mutableStateFlow = MutableStateFlow(
|
private val mutableStateFlow = MutableStateFlow(
|
||||||
PasswordHistoryState(PasswordHistoryState.ViewState.Loading),
|
PasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Default,
|
||||||
|
viewState = PasswordHistoryState.ViewState.Loading,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val viewModel = mockk<PasswordHistoryViewModel>(relaxed = true) {
|
private val viewModel = mockk<PasswordHistoryViewModel>(relaxed = true) {
|
||||||
|
@ -41,16 +51,22 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Empty state should display no passwords message`() {
|
fun `Empty state should display no passwords message`() {
|
||||||
updateState(PasswordHistoryState(PasswordHistoryState.ViewState.Empty))
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = PasswordHistoryState.ViewState.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
composeTestRule.onNodeWithText("No passwords to list.").assertIsDisplayed()
|
composeTestRule.onNodeWithText("No passwords to list.").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Error state should display error message`() {
|
fun `Error state should display error message`() {
|
||||||
val errorMessage = "Error occurred"
|
val errorMessage = "Error occurred"
|
||||||
updateState(
|
mutableStateFlow.update {
|
||||||
PasswordHistoryState(PasswordHistoryState.ViewState.Error(errorMessage.asText())),
|
it.copy(
|
||||||
)
|
viewState = PasswordHistoryState.ViewState.Error(errorMessage.asText()),
|
||||||
|
)
|
||||||
|
}
|
||||||
composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed()
|
composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,13 +90,13 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `clicking the Copy button should send PasswordCopyClick action`() {
|
fun `clicking the Copy button should send PasswordCopyClick action`() {
|
||||||
val password = PasswordHistoryState.GeneratedPassword(password = "Password", date = "Date")
|
val password = PasswordHistoryState.GeneratedPassword(password = "Password", date = "Date")
|
||||||
updateState(
|
mutableStateFlow.update {
|
||||||
PasswordHistoryState(
|
it.copy(
|
||||||
PasswordHistoryState.ViewState.Content(
|
viewState = PasswordHistoryState.ViewState.Content(
|
||||||
passwords = listOf(password),
|
passwords = listOf(password),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(password.password).assertIsDisplayed()
|
composeTestRule.onNodeWithText(password.password).assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithContentDescription("Copy").performClick()
|
composeTestRule.onNodeWithContentDescription("Copy").performClick()
|
||||||
|
@ -109,18 +125,44 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Clear button should depend on state`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Clear")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(label = "More")
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Clear")
|
||||||
|
.filterToOne(hasAnyAncestor(isPopup()))
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Content state should display list of passwords`() {
|
fun `Content state should display list of passwords`() {
|
||||||
val passwords =
|
val passwords =
|
||||||
listOf(PasswordHistoryState.GeneratedPassword(password = "Password1", date = "Date1"))
|
listOf(PasswordHistoryState.GeneratedPassword(password = "Password1", date = "Date1"))
|
||||||
|
|
||||||
updateState(
|
mutableStateFlow.update {
|
||||||
PasswordHistoryState(
|
it.copy(
|
||||||
PasswordHistoryState.ViewState.Content(
|
viewState = PasswordHistoryState.ViewState.Content(
|
||||||
passwords = passwords,
|
passwords = passwords,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("Password1").assertIsDisplayed()
|
composeTestRule.onNodeWithText("Password1").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
import com.bitwarden.core.PasswordHistoryView
|
import com.bitwarden.core.PasswordHistoryView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
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.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
@ -22,10 +29,14 @@ import java.time.Instant
|
||||||
|
|
||||||
class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val initialState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading)
|
private val initialState = createPasswordHistoryState()
|
||||||
|
|
||||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||||
private val fakeGeneratorRepository = FakeGeneratorRepository()
|
private val fakeGeneratorRepository = FakeGeneratorRepository()
|
||||||
|
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||||
|
private val fakeVaultRepository: VaultRepository = mockk {
|
||||||
|
every { getVaultItemStateFlow("mockId-1") } returns mutableVaultItemFlow
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct`() = runTest {
|
fun `initial state should be correct`() = runTest {
|
||||||
|
@ -41,7 +52,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
val expectedState = PasswordHistoryState(PasswordHistoryState.ViewState.Loading)
|
val expectedState = createPasswordHistoryState()
|
||||||
val actualState = awaitItem()
|
val actualState = awaitItem()
|
||||||
assertEquals(expectedState, actualState)
|
assertEquals(expectedState, actualState)
|
||||||
}
|
}
|
||||||
|
@ -55,8 +66,10 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
val expectedState = PasswordHistoryState(
|
val expectedState = createPasswordHistoryState(
|
||||||
PasswordHistoryState.ViewState.Error(R.string.an_error_has_occurred.asText()),
|
viewState = PasswordHistoryState.ViewState.Error(
|
||||||
|
message = R.string.an_error_has_occurred.asText(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val actualState = awaitItem()
|
val actualState = awaitItem()
|
||||||
assertEquals(expectedState, actualState)
|
assertEquals(expectedState, actualState)
|
||||||
|
@ -69,7 +82,92 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
val expectedState = PasswordHistoryState(PasswordHistoryState.ViewState.Empty)
|
val expectedState = createPasswordHistoryState(
|
||||||
|
viewState = PasswordHistoryState.ViewState.Empty,
|
||||||
|
)
|
||||||
|
val actualState = awaitItem()
|
||||||
|
assertEquals(expectedState, actualState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when VaultRepository emits Loading state the state updates correctly`() = runTest {
|
||||||
|
mutableVaultItemFlow.value = DataState.Loading
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = createPasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
val expectedState = createPasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
)
|
||||||
|
val actualState = awaitItem()
|
||||||
|
assertEquals(expectedState, actualState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when VaultRepository emits Error state the state updates correctly`() = runTest {
|
||||||
|
mutableVaultItemFlow.value = DataState.Error(error = IllegalStateException())
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = createPasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
val expectedState = createPasswordHistoryState(
|
||||||
|
viewState = PasswordHistoryState.ViewState.Error(
|
||||||
|
message = R.string.an_error_has_occurred.asText(),
|
||||||
|
),
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
)
|
||||||
|
val actualState = awaitItem()
|
||||||
|
assertEquals(expectedState, actualState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when VaultRepository emits Empty state the state updates correctly`() = runTest {
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(null)
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = createPasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
val expectedState = createPasswordHistoryState(
|
||||||
|
viewState = PasswordHistoryState.ViewState.Empty,
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
)
|
||||||
|
val actualState = awaitItem()
|
||||||
|
assertEquals(expectedState, actualState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when VaultRepository emits Pending state the state updates correctly`() = runTest {
|
||||||
|
mutableVaultItemFlow.value = DataState.Pending(createMockCipherView(1))
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = createPasswordHistoryState(
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
val expectedState = createPasswordHistoryState(
|
||||||
|
viewState = PasswordHistoryState.ViewState.Content(
|
||||||
|
passwords = listOf(
|
||||||
|
PasswordHistoryState.GeneratedPassword(
|
||||||
|
password = "mockPassword-1",
|
||||||
|
date = "10/27/23 6:00 AM",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = "mockId-1"),
|
||||||
|
)
|
||||||
val actualState = awaitItem()
|
val actualState = awaitItem()
|
||||||
assertEquals(expectedState, actualState)
|
assertEquals(expectedState, actualState)
|
||||||
}
|
}
|
||||||
|
@ -82,7 +180,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
|
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
|
||||||
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
|
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
|
||||||
|
|
||||||
val expectedState = PasswordHistoryState(
|
val expectedState = createPasswordHistoryState(
|
||||||
viewState = PasswordHistoryState.ViewState.Content(
|
viewState = PasswordHistoryState.ViewState.Content(
|
||||||
passwords = listOf(
|
passwords = listOf(
|
||||||
PasswordHistoryState.GeneratedPassword(
|
PasswordHistoryState.GeneratedPassword(
|
||||||
|
@ -146,10 +244,41 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
//region Helper Functions
|
//region Helper Functions
|
||||||
|
|
||||||
private fun createViewModel(): PasswordHistoryViewModel = PasswordHistoryViewModel(
|
private fun createViewModel(
|
||||||
|
initialState: PasswordHistoryState = createPasswordHistoryState(),
|
||||||
|
): PasswordHistoryViewModel = PasswordHistoryViewModel(
|
||||||
|
savedStateHandle = createSavedStateHandleWithState(state = initialState),
|
||||||
clipboardManager = clipboardManager,
|
clipboardManager = clipboardManager,
|
||||||
generatorRepository = fakeGeneratorRepository,
|
generatorRepository = fakeGeneratorRepository,
|
||||||
|
vaultRepository = fakeVaultRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun createPasswordHistoryState(
|
||||||
|
viewState: PasswordHistoryState.ViewState = PasswordHistoryState.ViewState.Loading,
|
||||||
|
passwordHistoryMode: GeneratorPasswordHistoryMode = GeneratorPasswordHistoryMode.Default,
|
||||||
|
): PasswordHistoryState =
|
||||||
|
PasswordHistoryState(
|
||||||
|
viewState = viewState,
|
||||||
|
passwordHistoryMode = passwordHistoryMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createSavedStateHandleWithState(
|
||||||
|
state: PasswordHistoryState? = createPasswordHistoryState(),
|
||||||
|
passwordHistoryMode: GeneratorPasswordHistoryMode = GeneratorPasswordHistoryMode.Default,
|
||||||
|
) = SavedStateHandle().apply {
|
||||||
|
set("state", state)
|
||||||
|
set(
|
||||||
|
"password_history_mode",
|
||||||
|
when (passwordHistoryMode) {
|
||||||
|
is GeneratorPasswordHistoryMode.Default -> "default"
|
||||||
|
is GeneratorPasswordHistoryMode.Item -> "item"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
set(
|
||||||
|
"password_history_id",
|
||||||
|
(passwordHistoryMode as? GeneratorPasswordHistoryMode.Item)?.itemId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Helper Functions
|
//endregion Helper Functions
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
private var onNavigateToVaultEditItemId: String? = null
|
private var onNavigateToVaultEditItemId: String? = null
|
||||||
private var onNavigateToMoveToOrganizationItemId: String? = null
|
private var onNavigateToMoveToOrganizationItemId: String? = null
|
||||||
private var onNavigateToAttachmentsId: String? = null
|
private var onNavigateToAttachmentsId: String? = null
|
||||||
|
private var onNavigateToPasswordHistoryId: String? = null
|
||||||
|
|
||||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
onNavigateToMoveToOrganizationItemId = id
|
onNavigateToMoveToOrganizationItemId = id
|
||||||
},
|
},
|
||||||
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
||||||
|
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryId = it },
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -107,6 +109,13 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
assertEquals(id, onNavigateToAttachmentsId)
|
assertEquals(id, onNavigateToAttachmentsId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToPasswordHistory event should invoke onNavigateToPasswordHistory`() {
|
||||||
|
val id = "id1234"
|
||||||
|
mutableEventFlow.tryEmit(VaultItemEvent.NavigateToPasswordHistory(itemId = id))
|
||||||
|
assertEquals(id, onNavigateToPasswordHistoryId)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on close click should send CloseClick`() {
|
fun `on close click should send CloseClick`() {
|
||||||
composeTestRule.onNodeWithContentDescription(label = "Close").performClick()
|
composeTestRule.onNodeWithContentDescription(label = "Close").performClick()
|
||||||
|
|
|
@ -104,6 +104,8 @@ class VaultAddItemStateExtensionsTest {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `toCipherView should transform Login ItemType to CipherView with original cipher`() {
|
fun `toCipherView should transform Login ItemType to CipherView with original cipher`() {
|
||||||
|
mockkStatic(Instant::class)
|
||||||
|
every { Instant.now() } returns Instant.MIN
|
||||||
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
||||||
val viewState = VaultAddEditState.ViewState.Content(
|
val viewState = VaultAddEditState.ViewState.Content(
|
||||||
common = VaultAddEditState.ViewState.Content.Common(
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
|
@ -147,7 +149,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
login = LoginView(
|
login = LoginView(
|
||||||
username = "mockUsername-1",
|
username = "mockUsername-1",
|
||||||
password = "mockPassword-1",
|
password = "mockPassword-1",
|
||||||
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
|
passwordRevisionDate = Instant.MIN,
|
||||||
uris = listOf(
|
uris = listOf(
|
||||||
LoginUriView(
|
LoginUriView(
|
||||||
uri = "mockUri-1",
|
uri = "mockUri-1",
|
||||||
|
@ -186,9 +188,13 @@ class VaultAddItemStateExtensionsTest {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
passwordHistory = listOf(
|
passwordHistory = listOf(
|
||||||
|
PasswordHistoryView(
|
||||||
|
password = "password",
|
||||||
|
lastUsedDate = Instant.MIN,
|
||||||
|
),
|
||||||
PasswordHistoryView(
|
PasswordHistoryView(
|
||||||
password = "old_password",
|
password = "old_password",
|
||||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
lastUsedDate = Instant.MIN,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -500,7 +506,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
passwordHistory = listOf(
|
passwordHistory = listOf(
|
||||||
PasswordHistoryView(
|
PasswordHistoryView(
|
||||||
password = "old_password",
|
password = "old_password",
|
||||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
lastUsedDate = Instant.MIN,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -564,12 +570,12 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||||
passwordHistory = listOf(
|
passwordHistory = listOf(
|
||||||
PasswordHistoryView(
|
PasswordHistoryView(
|
||||||
password = "old_password",
|
password = "old_password",
|
||||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
lastUsedDate = Instant.MIN,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
creationDate = Instant.ofEpochSecond(1_000L),
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.ofEpochSecond(1_000L),
|
revisionDate = Instant.MIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||||
|
@ -577,7 +583,7 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop
|
||||||
login = LoginView(
|
login = LoginView(
|
||||||
username = "username",
|
username = "username",
|
||||||
password = "password",
|
password = "password",
|
||||||
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
|
passwordRevisionDate = Instant.MIN,
|
||||||
uris = listOf(
|
uris = listOf(
|
||||||
LoginUriView(
|
LoginUriView(
|
||||||
uri = "www.example.com",
|
uri = "www.example.com",
|
||||||
|
|
Loading…
Reference in a new issue