PM-18123 Update the reset password screen. (#4719)

This commit is contained in:
Dave Severns
2025-02-13 16:07:17 -05:00
committed by GitHub
parent 1198de2a74
commit 9f616b67c9
22 changed files with 390 additions and 98 deletions

View File

@@ -362,8 +362,10 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Get the password strength for the given [email] and [password] combo.
* If no value is passed for the [email] will use the active email of the current active
* account via the [userStateFlow].
*/
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
/**
* Validates the master password for the current logged in user.

View File

@@ -1196,12 +1196,17 @@ class AuthRepositoryImpl(
)
override suspend fun getPasswordStrength(
email: String,
email: String?,
password: String,
): PasswordStrengthResult =
authSdkSource
.passwordStrength(
email = email,
email = email
?: userStateFlow
.value
?.activeAccount
?.email
.orEmpty(),
password = password,
)
.fold(

View File

@@ -105,7 +105,7 @@ private fun RemovePasswordScreenContent(
value = state.input,
onValueChange = onInputChanged,
showPasswordTestTag = "PasswordVisibilityToggle",
textFieldTestTag = "MasterPasswordEntry",
passwordFieldTestTag = "MasterPasswordEntry",
autoFocus = true,
cardStyle = CardStyle.Full,
modifier = Modifier

View File

@@ -10,17 +10,21 @@ const val RESET_PASSWORD_ROUTE: String = "reset_password"
/**
* Add the Reset Password screen to the nav graph.
*/
fun NavGraphBuilder.resetPasswordDestination() {
fun NavGraphBuilder.resetPasswordDestination(
onNavigateToPreventAccountLockOut: () -> Unit,
) {
composable(
route = RESET_PASSWORD_ROUTE,
) {
ResetPasswordScreen()
ResetPasswordScreen(onNavigateToPreventAccountLockOut = onNavigateToPreventAccountLockOut)
}
}
/**
* Navigate to the Reset Password screen.
*/
fun NavController.navigateToResetPasswordGraph(navOptions: NavOptions? = null) {
fun NavController.navigateToResetPasswordScreen(
navOptions: NavOptions? = null,
) {
this.navigate(RESET_PASSWORD_ROUTE, navOptions)
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.resetpassword
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -23,13 +25,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
@@ -39,6 +47,9 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.persistentListOf
/**
* The top level composable for the Reset Password screen.
@@ -47,10 +58,19 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@Composable
@Suppress("LongMethod")
fun ResetPasswordScreen(
onNavigateToPreventAccountLockOut: () -> Unit,
viewModel: ResetPasswordViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
ResetPasswordEvent.NavigateToPreventAccountLockout -> {
onNavigateToPreventAccountLockOut()
}
}
}
when (val dialog = state.dialogState) {
is ResetPasswordState.DialogState.Error -> {
BitwardenBasicDialog(
@@ -69,7 +89,7 @@ fun ResetPasswordScreen(
null -> Unit
}
var shouldShowLogoutConfirmationDialog by remember { mutableStateOf(false) }
var shouldShowLogoutConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val onLogoutClicked = remember(viewModel) {
{ viewModel.trySendAction(ResetPasswordAction.ConfirmLogoutClick) }
}
@@ -100,16 +120,19 @@ fun ResetPasswordScreen(
scrollBehavior = scrollBehavior,
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.log_out),
onClick = { shouldShowLogoutConfirmationDialog = true },
modifier = Modifier.testTag("LogoutButton"),
)
BitwardenTextButton(
label = stringResource(id = R.string.submit),
label = stringResource(id = R.string.save),
onClick = remember(viewModel) {
{ viewModel.trySendAction(ResetPasswordAction.SubmitClick) }
{ viewModel.trySendAction(ResetPasswordAction.SaveClick) }
},
modifier = Modifier.testTag("SubmitButton"),
modifier = Modifier.testTag("SaveButton"),
)
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
text = stringResource(R.string.log_out),
onClick = { shouldShowLogoutConfirmationDialog = true },
),
),
)
},
)
@@ -129,6 +152,9 @@ fun ResetPasswordScreen(
onPasswordHintInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it)) }
},
onLearnToPreventLockout = remember(viewModel) {
{ viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick) }
},
modifier = Modifier.fillMaxSize(),
)
}
@@ -142,6 +168,7 @@ private fun ResetPasswordScreenContent(
onPasswordInputChanged: (String) -> Unit,
onRetypePasswordInputChanged: (String) -> Unit,
onPasswordHintInputChanged: (String) -> Unit,
onLearnToPreventLockout: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -180,12 +207,11 @@ private fun ResetPasswordScreenContent(
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.current_master_password),
label = stringResource(id = R.string.current_master_password_required),
value = state.currentPasswordInput,
onValueChange = onCurrentPasswordInputChanged,
textFieldTestTag = "MasterPasswordField",
passwordFieldTestTag = "MasterPasswordField",
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.standardHorizontalMargin()
@@ -195,12 +221,20 @@ private fun ResetPasswordScreenContent(
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
label = stringResource(id = R.string.new_master_password_required),
value = state.passwordInput,
onValueChange = onPasswordInputChanged,
showPassword = isPasswordVisible,
showPasswordChange = { isPasswordVisible = it },
passwordFieldTestTag = "NewPasswordField",
supportingContent = {
PasswordStrengthIndicator(
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
minimumCharacterCount = state.minimumPasswordLength,
modifier = Modifier.fillMaxWidth(),
)
},
cardStyle = if (
state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
CardStyle.Middle(dividerPadding = 0.dp)
@@ -213,7 +247,7 @@ private fun ResetPasswordScreenContent(
)
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
label = stringResource(id = R.string.retype_new_master_password_required),
value = state.retypePasswordInput,
onValueChange = onRetypePasswordInputChanged,
showPassword = isPasswordVisible,
@@ -226,10 +260,29 @@ private fun ResetPasswordScreenContent(
)
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
label = stringResource(id = R.string.new_master_password_hint),
value = state.passwordHintInput,
onValueChange = onPasswordHintInputChanged,
supportingText = stringResource(id = R.string.master_password_hint_description),
supportingContent = {
Column {
Text(
text = stringResource(
R.string.bitwarden_cannot_reset_a_lost_or_forgotten_master_password,
),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
BitwardenHyperTextLink(
annotatedResId = R.string.learn_about_ways_to_prevent_account_lockout,
annotationKey = "onPreventAccountLockout",
accessibilityString = stringResource(
R.string.learn_about_ways_to_prevent_account_lockout,
),
onClick = onLearnToPreventLockout,
)
}
},
textFieldTestTag = "MasterPasswordHintLabel",
cardStyle = CardStyle.Bottom,
modifier = Modifier
@@ -240,3 +293,31 @@ private fun ResetPasswordScreenContent(
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Preview
@Composable
private fun ResetPasswordScreenContent_preview() {
BitwardenTheme {
ResetPasswordScreenContent(
state = ResetPasswordState(
policies = listOf(),
resetReason = null,
dialogState = null,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.GOOD,
minimumPasswordLength = 12,
),
onCurrentPasswordInputChanged = {},
onPasswordInputChanged = {},
onRetypePasswordInputChanged = {},
onPasswordHintInputChanged = {},
onLearnToPreventLockout = {},
modifier = Modifier
.fillMaxSize()
.background(BitwardenTheme.colorScheme.background.primary),
)
}
}

View File

@@ -5,15 +5,20 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
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.orNullIfBlank
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toStrictestPolicy
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
@@ -34,16 +39,30 @@ class ResetPasswordViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<ResetPasswordState, ResetPasswordEvent, ResetPasswordAction>(
initialState = savedStateHandle[KEY_STATE]
?: ResetPasswordState(
policies = authRepository.passwordPolicies.toDisplayLabels(),
resetReason = authRepository.passwordResetReason,
dialogState = null,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
),
?: run {
val policies = authRepository.passwordPolicies
ResetPasswordState(
policies = policies.toDisplayLabels(),
resetReason = authRepository.passwordResetReason,
dialogState = null,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = policies
.toStrictestPolicy()
.minLength
?: MIN_PASSWORD_LENGTH,
)
},
) {
/**
* Keeps track of async request to get password strength. Should be cancelled
* when user input changes.
*/
private var passwordStrengthJob: Job = Job().apply { complete() }
init {
// As state updates, write to saved state handle.
stateFlow
@@ -54,7 +73,7 @@ class ResetPasswordViewModel @Inject constructor(
override fun handleAction(action: ResetPasswordAction) {
when (action) {
ResetPasswordAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
ResetPasswordAction.SubmitClick -> handleSubmitClicked()
ResetPasswordAction.SaveClick -> handleSaveClicked()
ResetPasswordAction.DialogDismiss -> handleDialogDismiss()
is ResetPasswordAction.CurrentPasswordInputChanged -> {
@@ -82,6 +101,58 @@ class ResetPasswordViewModel @Inject constructor(
is ResetPasswordAction.Internal.ReceiveValidatePasswordResult -> {
handleReceiveValidatePasswordResult(action)
}
is ResetPasswordAction.Internal.ReceivePasswordStrengthResult -> {
handlePasswordStrengthResult(action)
}
ResetPasswordAction.LearnHowPreventLockoutClick -> {
handleLearnHowToPreventLockoutClick()
}
}
}
private fun handleLearnHowToPreventLockoutClick() {
sendEvent(ResetPasswordEvent.NavigateToPreventAccountLockout)
}
private fun checkPasswordStrength(input: String) {
// Update password strength:
passwordStrengthJob.cancel()
if (input.isEmpty()) {
mutableStateFlow.update {
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
}
} else {
passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength(
password = input,
)
trySendAction(ResetPasswordAction.Internal.ReceivePasswordStrengthResult(result))
}
}
}
private fun handlePasswordStrengthResult(
action: ResetPasswordAction.Internal.ReceivePasswordStrengthResult,
) {
when (val result = action.result) {
is PasswordStrengthResult.Success -> {
val updatedState = when (result.passwordStrength) {
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
}
mutableStateFlow.update {
it.copy(
passwordStrengthState = updatedState,
)
}
}
PasswordStrengthResult.Error -> Unit
}
}
@@ -95,7 +166,7 @@ class ResetPasswordViewModel @Inject constructor(
/**
* Validate the user's current password when they submit.
*/
private fun handleSubmitClicked() {
private fun handleSaveClicked() {
// Display an error dialog if the new password field is blank.
if (state.passwordInput.isBlank()) {
mutableStateFlow.update {
@@ -175,6 +246,7 @@ class ResetPasswordViewModel @Inject constructor(
passwordInput = action.input,
)
}
checkPasswordStrength(input = action.input)
}
/**
@@ -354,6 +426,8 @@ data class ResetPasswordState(
val passwordInput: String,
val retypePasswordInput: String,
val passwordHintInput: String,
val passwordStrengthState: PasswordStrengthState,
val minimumPasswordLength: Int,
) : Parcelable {
/**
* Represents the current state of any dialogs on the screen.
@@ -382,7 +456,13 @@ data class ResetPasswordState(
/**
* Models events for the Reset Password screen.
*/
sealed class ResetPasswordEvent
sealed class ResetPasswordEvent {
/**
* Navigate to the LearnToPreventLockout screen
*/
data object NavigateToPreventAccountLockout : ResetPasswordEvent()
}
/**
* Models actions for the Reset Password screen.
@@ -394,9 +474,9 @@ sealed class ResetPasswordAction {
data object ConfirmLogoutClick : ResetPasswordAction()
/**
* Indicates that the user has clicked the submit button.
* Indicates that the user has clicked the save button.
*/
data object SubmitClick : ResetPasswordAction()
data object SaveClick : ResetPasswordAction()
/**
* Indicates that the dialog has been dismissed.
@@ -423,6 +503,11 @@ sealed class ResetPasswordAction {
*/
data class PasswordHintInputChanged(val input: String) : ResetPasswordAction()
/**
* Indicates user clicked the "Learn ways..." text
*/
data object LearnHowPreventLockoutClick : ResetPasswordAction()
/**
* Models actions that the [ResetPasswordViewModel] might send itself.
*/
@@ -447,5 +532,12 @@ sealed class ResetPasswordAction {
data class ReceiveValidatePasswordAgainstPoliciesResult(
val meetsRequirements: Boolean,
) : Internal()
/**
* Indicates a password strength result has been received.
*/
data class ReceivePasswordStrengthResult(
val result: PasswordStrengthResult,
) : Internal()
}
}

View File

@@ -269,7 +269,7 @@ fun VaultUnlockScreen(
},
),
supportingText = state.vaultUnlockType.unlockScreenMessage(),
textFieldTestTag = state.vaultUnlockType.unlockScreenInputTestTag,
passwordFieldTestTag = state.vaultUnlockType.unlockScreenInputTestTag,
cardStyle = CardStyle.Top(hasDivider = false),
modifier = Modifier
.standardHorizontalMargin()

View File

@@ -9,7 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -45,7 +45,7 @@ fun BitwardenOverflowActionItem(
menuItemDataList: ImmutableList<OverflowMenuItemData> = persistentListOf(),
) {
if (menuItemDataList.isEmpty()) return
var isOverflowMenuVisible by remember { mutableStateOf(false) }
var isOverflowMenuVisible by rememberSaveable { mutableStateOf(false) }
Box(
contentAlignment = Alignment.Center,
modifier = modifier,

View File

@@ -94,7 +94,7 @@ fun BitwardenClientCertificateDialog(
value = password,
onValueChange = { password = it },
cardStyle = CardStyle.Bottom,
textFieldTestTag = "AlertClientCertificatePasswordInputField",
passwordFieldTestTag = "AlertClientCertificatePasswordInputField",
modifier = Modifier.imePadding(),
)
}

View File

@@ -78,7 +78,7 @@ fun BitwardenMasterPasswordDialog(
value = masterPassword,
onValueChange = { masterPassword = it },
autoFocus = true,
textFieldTestTag = "AlertInputField",
passwordFieldTestTag = "AlertInputField",
cardStyle = CardStyle.Full,
modifier = Modifier.imePadding(),
)

View File

@@ -66,7 +66,7 @@ fun BitwardenPinDialog(
value = pin,
onValueChange = { pin = it },
autoFocus = true,
textFieldTestTag = "AlertInputField",
passwordFieldTestTag = "AlertInputField",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()

View File

@@ -398,7 +398,7 @@ fun BitwardenPasswordField(
* the password field.
* @param imeAction the preferred IME action for the keyboard to have.
* @param keyboardActions the callbacks of keyboard actions.
* @param textFieldTestTag The optional test tag associated with the inner text field.
* @param passwordFieldTestTag The optional test tag associated with the inner text field.
* @param cardStyle Indicates the type of card style to be applied.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
@@ -421,7 +421,7 @@ fun BitwardenPasswordField(
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
textFieldTestTag: String? = null,
passwordFieldTestTag: String? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actions: @Composable (RowScope.() -> Unit)? = null,
) {
@@ -441,7 +441,7 @@ fun BitwardenPasswordField(
keyboardType = keyboardType,
imeAction = imeAction,
keyboardActions = keyboardActions,
passwordFieldTestTag = textFieldTestTag,
passwordFieldTestTag = passwordFieldTestTag,
cardStyle = cardStyle,
actionsPadding = actionsPadding,
actions = actions,

View File

@@ -29,11 +29,12 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.navigateToExpiredRegistrationLinkScreen
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.navigateToNewDeviceNoticeEmailAccess
import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.navigateToPreventAccountLockout
import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordGraph
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordScreen
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.setpassword.SET_PASSWORD_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
@@ -94,7 +95,11 @@ fun RootNavScreen(
splashDestination()
authGraph(navController)
removePasswordDestination()
resetPasswordDestination()
resetPasswordDestination(
onNavigateToPreventAccountLockOut = {
navController.navigateToPreventAccountLockout()
},
)
trustedDeviceGraph(navController)
vaultUnlockDestination()
vaultUnlockedGraph(navController)
@@ -182,7 +187,9 @@ fun RootNavScreen(
}
RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
RootNavState.ResetPassword -> {
navController.navigateToResetPasswordScreen(rootNavOptions)
}
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)

View File

@@ -321,7 +321,7 @@ private fun ExportVaultScreenContent(
value = state.passwordInput,
readOnly = state.policyPreventsExport,
onValueChange = onPasswordInputChanged,
textFieldTestTag = "MasterPasswordEntry",
passwordFieldTestTag = "MasterPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()

View File

@@ -54,7 +54,6 @@ class ExportVaultViewModel @Inject constructor(
?: ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
email = requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
exportData = null,
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
@@ -266,7 +265,6 @@ class ExportVaultViewModel @Inject constructor(
} else {
passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength(
email = state.email,
password = action.input,
)
trySendAction(ExportVaultAction.Internal.ReceivePasswordStrengthResult(result))
@@ -458,7 +456,6 @@ data class ExportVaultState(
val exportData: String? = null,
val confirmFilePasswordInput: String,
val dialogState: DialogState?,
val email: String,
val exportFormat: ExportVaultFormat,
val filePasswordInput: String,
val passwordInput: String,

View File

@@ -1207,7 +1207,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiAccessToken,
onValueChange = forwardedEmailAliasHandlers.onAddyIoAccessTokenTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1234,7 +1234,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onDuckDuckGoApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1248,7 +1248,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onFastMailApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1262,7 +1262,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiAccessToken,
onValueChange = forwardedEmailAliasHandlers.onFirefoxRelayAccessTokenTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1276,7 +1276,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1303,7 +1303,7 @@ private fun ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onSimpleLoginApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
@@ -1318,7 +1318,7 @@ private fun ForwardedEmailAliasTypeContent(
value = obfuscatedTextField,
onValueChange = { obfuscatedTextField = it },
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
textFieldTestTag = "ForwardedEmailApiSecretEntry",
passwordFieldTestTag = "ForwardedEmailApiSecretEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()

View File

@@ -377,7 +377,7 @@ private fun AddSendOptions(
readOnly = sendRestrictionPolicy,
value = state.common.passwordInput,
onValueChange = addSendHandlers.onPasswordChange,
textFieldTestTag = "SendNewPasswordEntry",
passwordFieldTestTag = "SendNewPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()

View File

@@ -50,6 +50,7 @@
<string name="switch_to_already_added_account_confirmation">Would you like to switch to it now?</string>
<string name="master_password">Master password</string>
<string name="master_password_required">Master password (required)</string>
<string name="new_master_password_required">New master password (required)</string>
<string name="more">More</string>
<string name="my_vault">My vault</string>
<string name="authenticator">Authenticator</string>
@@ -186,6 +187,7 @@
<string name="rate_the_app_description">Please consider helping us out with a good review!</string>
<string name="regenerate_password">Regenerate password</string>
<string name="retype_master_password">Re-type master password</string>
<string name="retype_new_master_password_required">Re-type new master password (required)</string>
<string name="retype_master_password_required">Re-type master password (required)</string>
<string name="search_vault">Search vault</string>
<string name="security">Security</string>
@@ -829,7 +831,7 @@ Do you want to switch to this account?</string>
<string name="data_region">Data region</string>
<string name="region">Region</string>
<string name="update_weak_master_password_warning">Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</string>
<string name="current_master_password">Current master password</string>
<string name="current_master_password_required">Current master password (required)</string>
<string name="logged_in">Logged in!</string>
<string name="approve_with_my_other_device">Approve with my other device</string>
<string name="request_admin_approval">Request admin approval</string>
@@ -1021,12 +1023,14 @@ Do you want to switch to this account?</string>
<string name="generate_button_label">Generate</string>
<string name="write_this_password_down_and_keep_it_somewhere_safe">Write this password down and keep it somewhere safe.</string>
<string name="learn_about_other_ways_to_prevent_account_lockout">Learn about other ways to prevent account lockout</string>
<string name="learn_about_ways_to_prevent_account_lockout"><annotation link="onPreventAccountLockout">Learn about ways to prevent account lockout</annotation></string>
<string name="help_with_server_geolocations">Help with server geolocations.</string>
<string name="email_address_required">Email address (required)</string>
<string name="select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account">"Select the link in the email to verify your email address and continue creating your account. "</string>
<string name="change_email_address">Change email address</string>
<string name="next">Next</string>
<string name="bitwarden_cannot_recover_a_lost_or_forgotten_master_password">Bitwarden cannot recover a lost or forgotten master password.</string>
<string name="bitwarden_cannot_reset_a_lost_or_forgotten_master_password">Bitwarden cannot reset a lost or forgotten master password.</string>
<string name="choose_your_master_password">Choose your master password</string>
<string name="choose_a_unique_and_strong_password_to_keep_your_information_safe">Choose a unique and strong password to keep your information safe.</string>
<string name="minimum_characters">%1$s characters</string>
@@ -1046,6 +1050,7 @@ Do you want to switch to this account?</string>
<string name="error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance">Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance.</string>
<string name="master_password_hint_not_specified">Master password hint</string>
<string name="master_password_important_hint">Important: Your master password cannot be recovered if you forget it! 12 characters minimum.</string>
<string name="new_master_password_hint">New master password hint</string>
<string name="get_started">Get started</string>
<string name="save_and_protect_your_data">Save and protect your data</string>
<string name="the_vault_protects_more_than_just_passwords">The vault protects more than just passwords. Store secure logins, IDs, cards and notes securely here.</string>

View File

@@ -7,11 +7,13 @@ import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordAction
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordEvent
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordScreen
@@ -20,15 +22,18 @@ import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordViewModel
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ResetPasswordScreenTest : BaseComposeTest() {
private var onNavigateToLearnToPreventLockoutCalled = false
private val mutableEventFlow = bufferedMutableSharedFlow<ResetPasswordEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<ResetPasswordViewModel>(relaxed = true) {
@@ -40,6 +45,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
fun setUp() {
composeTestRule.setContent {
ResetPasswordScreen(
onNavigateToPreventAccountLockOut = {
onNavigateToLearnToPreventLockoutCalled = true
},
viewModel = viewModel,
)
}
@@ -82,9 +90,16 @@ class ResetPasswordScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithText("Loading...").isDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `logout button click should display confirmation dialog and emit ConfirmLogoutClick`() {
composeTestRule.onNodeWithText("Log out").performClick()
fun `logout button click from more menu should display confirmation dialog and emit ConfirmLogoutClick`() {
composeTestRule
.onNodeWithContentDescription("More")
.performClick()
composeTestRule
.onNodeWithText("Log out")
.performClick()
composeTestRule
.onNodeWithText("Are you sure you want to log out?")
@@ -104,10 +119,12 @@ class ResetPasswordScreenTest : BaseComposeTest() {
@Test
fun `submit button click should emit SubmitClick`() {
composeTestRule.onNodeWithText("Submit").performClick()
composeTestRule
.onNodeWithText("Save")
.performClick()
verify {
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
}
}
@@ -176,7 +193,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
@Test
fun `current password input change should send CurrentPasswordInputChanged action`() {
val input = "Test123"
composeTestRule.onNodeWithText("Current master password").performTextInput(input)
composeTestRule
.onNodeWithText("Current master password (required)")
.performTextInput(input)
verify {
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
}
@@ -185,7 +204,7 @@ class ResetPasswordScreenTest : BaseComposeTest() {
@Test
fun `current password field should update according to state`() {
composeTestRule
.onNodeWithText("Current master password")
.onNodeWithText("Current master password (required)")
.assertIsDisplayed()
mutableStateFlow.update {
@@ -195,14 +214,16 @@ class ResetPasswordScreenTest : BaseComposeTest() {
}
composeTestRule
.onNodeWithText("Current master password")
.onNodeWithText("Current master password (required)")
.assertDoesNotExist()
}
@Test
fun `password input change should send PasswordInputChange action`() {
val input = "Test123"
composeTestRule.onNodeWithText("Master password").performTextInput(input)
composeTestRule
.onNodeWithText("New master password (required)")
.performTextInput(input)
verify {
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
}
@@ -211,7 +232,9 @@ class ResetPasswordScreenTest : BaseComposeTest() {
@Test
fun `retype password input change should send RetypePasswordInputChanged action`() {
val input = "Test123"
composeTestRule.onNodeWithText("Re-type master password").performTextInput(input)
composeTestRule
.onNodeWithText("Re-type new master password (required)")
.performTextInput(input)
verify {
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
}
@@ -220,7 +243,7 @@ class ResetPasswordScreenTest : BaseComposeTest() {
@Test
fun `password hint input change should send PasswordHintInputChanged action`() {
val input = "Test123"
composeTestRule.onNodeWithText("Master password hint (optional)").performTextInput(input)
composeTestRule.onNodeWithText("New master password hint").performTextInput(input)
verify {
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
}
@@ -250,6 +273,21 @@ class ResetPasswordScreenTest : BaseComposeTest() {
.onAllNodesWithContentDescription("Show")
.assertCountEquals(3)
}
@Test
fun `NavigateToPreventAccountLockout event calls onNavigateToPreventAccountLockout`() {
mutableEventFlow.tryEmit(ResetPasswordEvent.NavigateToPreventAccountLockout)
assertTrue(onNavigateToLearnToPreventLockoutCalled)
}
@Test
fun `When learn new ways text is clicked, send correct action`() {
composeTestRule
.onNodeWithText("Learn about ways to prevent account lockout")
.performCustomAccessibilityAction("Learn about ways to prevent account lockout")
verify { viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick) }
}
}
private val DEFAULT_STATE = ResetPasswordState(
@@ -260,4 +298,6 @@ private val DEFAULT_STATE = ResetPasswordState(
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = 12,
)

View File

@@ -4,10 +4,14 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordAction
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordEvent
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordViewModel
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
@@ -57,7 +61,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClicked with blank password shows error alert`() {
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -84,10 +88,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
} returns false
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -96,6 +103,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
message = R.string.master_password_policy_validation_message.asText(),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -107,10 +115,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
every {
authRepository.passwordResetReason
} returns ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -121,6 +132,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
.asText(MIN_PASSWORD_LENGTH),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -132,11 +144,14 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
} returns true
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -145,6 +160,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
message = R.string.master_password_confirmation_val_message.asText(),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -160,13 +176,16 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.validatePassword(currentPassword)
} returns ValidatePasswordResult.Error
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -177,6 +196,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -192,13 +212,16 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.validatePassword(currentPassword)
} returns ValidatePasswordResult.Success(isValid = false)
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -209,6 +232,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -227,6 +251,9 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.resetPassword(any(), any(), any())
} returns ResetPasswordResult.Success
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
@@ -240,11 +267,12 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(
DEFAULT_STATE.copy(
@@ -254,6 +282,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
@@ -264,6 +293,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
@@ -272,18 +302,40 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `PasswordInputChanged should update the password input in the state`() {
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
fun `PasswordInputChanged should update the password input in the state along with the password strength state`() =
runTest {
val passwordInput = "Test123"
val viewModel = createViewModel()
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
assertEquals(
DEFAULT_STATE.copy(
passwordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = passwordInput))
assertEquals(
DEFAULT_STATE.copy(
passwordInput = passwordInput,
),
awaitItem(),
)
assertEquals(
DEFAULT_STATE.copy(
passwordInput = passwordInput,
passwordStrengthState = PasswordStrengthState.STRONG,
),
awaitItem(),
)
}
coVerify {
authRepository.getPasswordStrength(
password = passwordInput,
)
}
}
@Test
fun `RetypePasswordInputChanged should update the retype password input in the state`() {
@@ -311,6 +363,19 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `LearnHowPreventLockoutClick action sends NavigateToPreventAccountLockout event`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick)
assertEquals(
ResetPasswordEvent.NavigateToPreventAccountLockout,
awaitItem(),
)
}
}
private fun createViewModel(): ResetPasswordViewModel =
ResetPasswordViewModel(
authRepository = authRepository,
@@ -327,4 +392,6 @@ private val DEFAULT_STATE = ResetPasswordState(
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = 12,
)

View File

@@ -305,7 +305,6 @@ class ExportVaultScreenTest : BaseComposeTest() {
private val DEFAULT_STATE = ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
email = "test@bitwarden.com",
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
passwordInput = "",

View File

@@ -184,7 +184,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
)
coEvery {
authRepository.getPasswordStrength(
email = EMAIL_ADDRESS,
password = password,
)
} returns PasswordStrengthResult.Success(
@@ -280,7 +279,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
val password = "password"
coEvery {
authRepository.getPasswordStrength(
email = EMAIL_ADDRESS,
password = password,
)
} returns PasswordStrengthResult.Success(
@@ -334,7 +332,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
)
coEvery {
authRepository.getPasswordStrength(
email = EMAIL_ADDRESS,
password = password,
)
} returns PasswordStrengthResult.Success(
@@ -443,7 +440,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
val password = "Test123"
coEvery {
authRepository.getPasswordStrength(
email = EMAIL_ADDRESS,
password = password,
)
} returns PasswordStrengthResult.Success(
@@ -461,7 +457,6 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
)
coVerify {
authRepository.getPasswordStrength(
email = EMAIL_ADDRESS,
password = password,
)
}
@@ -750,14 +745,13 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
)
}
private const val EMAIL_ADDRESS = "active@bitwarden.com"
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Active User",
email = EMAIL_ADDRESS,
email = "email",
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
@@ -779,7 +773,6 @@ private val DEFAULT_USER_STATE = UserState(
private val DEFAULT_STATE = ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
email = EMAIL_ADDRESS,
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
passwordInput = "",