mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-1402 adding Password checker to Add Edit Screen (#769)
This commit is contained in:
parent
27a9719532
commit
54802db0b3
4 changed files with 142 additions and 48 deletions
|
@ -19,7 +19,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
|
@ -82,10 +81,12 @@ fun VaultAddEditScreen(
|
|||
is VaultAddEditEvent.NavigateToMoveToOrganization -> {
|
||||
onNavigateToMoveToOrganization(event.cipherId)
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.NavigateToCollections -> {
|
||||
// TODO implement Collections in BIT-1575
|
||||
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
}
|
||||
}
|
||||
|
@ -227,13 +228,15 @@ private fun VaultAddEditItemDialogs(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.DialogState.Error -> BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
is VaultAddEditState.DialogState.Generic -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialogState.title,
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
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.util.takeUntilLoaded
|
||||
|
@ -56,6 +58,7 @@ private const val KEY_STATE = "state"
|
|||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class VaultAddEditViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val generatorRepository: GeneratorRepository,
|
||||
|
@ -220,8 +223,9 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
if (content.common.name.isBlank()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
R.string.validation_field_required
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.name.asText()),
|
||||
),
|
||||
)
|
||||
|
@ -519,12 +523,15 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLoginPasswordCheckerClick() {
|
||||
viewModelScope.launch {
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(
|
||||
message = "Password Checker".asText(),
|
||||
),
|
||||
)
|
||||
onLoginType { loginType ->
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Loading(R.string.loading.asText()))
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.getPasswordBreachCount(password = loginType.password)
|
||||
sendAction(VaultAddEditAction.Internal.PasswordBreachReceive(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -847,6 +854,9 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is VaultAddEditAction.Internal.GeneratorResultReceive -> {
|
||||
handleGeneratorResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Internal.PasswordBreachReceive ->
|
||||
handlePasswordBreachReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -883,7 +893,8 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is UpdateCipherResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = result
|
||||
.errorMessage
|
||||
?.asText()
|
||||
|
@ -981,8 +992,9 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
TotpCodeResult.CodeScanningError -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
R.string.authenticator_key_read_error.asText(),
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.authenticator_key_read_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -1012,6 +1024,24 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePasswordBreachReceive(
|
||||
action: VaultAddEditAction.Internal.PasswordBreachReceive,
|
||||
) {
|
||||
val message = when (val result = action.result) {
|
||||
is BreachCountResult.Error -> R.string.generic_error_message.asText()
|
||||
is BreachCountResult.Success -> {
|
||||
if (result.breachCount > 0) {
|
||||
R.string.password_exposed.asText(result.breachCount)
|
||||
} else {
|
||||
R.string.password_safe.asText()
|
||||
}
|
||||
}
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Generic(message = message))
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
||||
//region Utility Functions
|
||||
|
@ -1049,6 +1079,12 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun onLoginType(
|
||||
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.Login) -> Unit,
|
||||
) {
|
||||
onContent { (it.type as? VaultAddEditState.ViewState.Content.ItemType.Login)?.let(block) }
|
||||
}
|
||||
|
||||
private inline fun updateLoginContent(
|
||||
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.Login) ->
|
||||
VaultAddEditState.ViewState.Content.ItemType.Login,
|
||||
|
@ -1427,17 +1463,20 @@ data class VaultAddEditState(
|
|||
@Parcelize
|
||||
sealed class DialogState : Parcelable {
|
||||
|
||||
/**
|
||||
* Displays a generic dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Generic(
|
||||
val title: Text? = null,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a loading dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(val label: Text) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays an error dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(val message: Text) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1884,6 +1923,11 @@ sealed class VaultAddEditAction {
|
|||
*/
|
||||
sealed class Internal : VaultAddEditAction() {
|
||||
|
||||
/**
|
||||
* Indicates that the password breach results have been received.
|
||||
*/
|
||||
data class PasswordBreachReceive(val result: BreachCountResult) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the vault totp code result has been received.
|
||||
*/
|
||||
|
|
|
@ -432,10 +432,14 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in ItemType_Login state clicking Password checker action should trigger PasswordCheckerClick`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(password = "password") }
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Password")
|
||||
.onSiblings()
|
||||
.onFirst()
|
||||
.filterToOne(hasContentDescription("Check if password has been exposed."))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
|
@ -2298,7 +2302,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
common = VaultAddEditState.ViewState.Content.Common(),
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
|
||||
),
|
||||
dialog = VaultAddEditState.DialogState.Error("test".asText()),
|
||||
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
)
|
||||
|
||||
|
|
|
@ -4,11 +4,14 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
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.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
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.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
|
@ -49,6 +52,8 @@ import java.util.UUID
|
|||
@Suppress("LargeClass")
|
||||
class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val authRepository: AuthRepository = mockk()
|
||||
|
||||
private val loginInitialState = createVaultAddItemState(
|
||||
typeContentViewState = createLoginTypeContentViewState(),
|
||||
)
|
||||
|
@ -402,7 +407,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
assertEquals(
|
||||
stateWithName.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
),
|
||||
|
@ -449,7 +455,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
assertEquals(
|
||||
stateWithName.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = errorMessage.asText(),
|
||||
),
|
||||
),
|
||||
|
@ -468,9 +475,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
val stateWithNoNameAndDialog = createVaultAddItemState(
|
||||
commonContentViewState = createCommonContentViewState(name = ""),
|
||||
dialogState = VaultAddEditState.DialogState.Error(
|
||||
R.string.validation_field_required
|
||||
.asText(R.string.name.asText()),
|
||||
dialogState = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required.asText(R.string.name.asText()),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -492,9 +499,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
fun `HandleDialogDismiss will remove the current dialog`() = runTest {
|
||||
val errorState = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
dialogState = VaultAddEditState.DialogState.Error(
|
||||
R.string.validation_field_required
|
||||
.asText(R.string.name.asText()),
|
||||
dialogState = VaultAddEditState.DialogState.Generic(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required.asText(R.string.name.asText()),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -687,24 +694,58 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `PasswordCheckerClick should emit ShowToast with 'Password Checker' message`() =
|
||||
runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
fun `on CheckForBreachClick should process a password`() = runTest {
|
||||
val cipherView = createMockCipherView(1)
|
||||
val password = "Password"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel
|
||||
.actionChannel
|
||||
.trySend(VaultAddEditAction.ItemType.LoginType.PasswordCheckerClick)
|
||||
val loginState = loginInitialState.copy(
|
||||
viewState = VaultAddEditState.ViewState.Content(
|
||||
common = createCommonContentViewState(),
|
||||
type = createLoginTypeContentViewState(
|
||||
password = password,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(
|
||||
"Password Checker".asText(),
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = loginState,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
),
|
||||
)
|
||||
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = cipherView)
|
||||
|
||||
val breachCount = 5
|
||||
coEvery {
|
||||
authRepository.getPasswordBreachCount(password)
|
||||
} returns BreachCountResult.Success(breachCount = breachCount)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.PasswordCheckerClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultAddEditState.DialogState.Loading(
|
||||
label = R.string.loading.asText(),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
message = R.string.password_exposed.asText(breachCount),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { authRepository.getPasswordBreachCount(password) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OpenPasswordGeneratorClick should emit NavigateToGeneratorModal with with password GeneratorMode`() =
|
||||
|
@ -1289,6 +1330,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository = vaultRepository,
|
||||
generatorRepository = generatorRepository,
|
||||
resourceManager = resourceManager,
|
||||
authRepository = authRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1796,6 +1838,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository = vaultRepo,
|
||||
generatorRepository = generatorRepo,
|
||||
resourceManager = bitwardenResourceManager,
|
||||
authRepository = authRepository,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue