Fix incorrect VM state access & unnecessary eventFlow usages (#1218)

This commit is contained in:
Caleb Derosier 2024-04-03 13:36:57 -06:00 committed by Álison Fernandes
parent ab55b5e535
commit 87254b4436
13 changed files with 381 additions and 430 deletions

View file

@ -191,7 +191,7 @@ class CreateAccountViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) } mutableStateFlow.update { it.copy(dialog = null) }
sendEvent( sendEvent(
CreateAccountEvent.NavigateToLogin( CreateAccountEvent.NavigateToLogin(
email = mutableStateFlow.value.emailInput, email = state.emailInput,
captchaToken = registerAccountResult.captchaToken, captchaToken = registerAccountResult.captchaToken,
), ),
) )
@ -251,7 +251,7 @@ class CreateAccountViewModel @Inject constructor(
} else { } else {
passwordStrengthJob = viewModelScope.launch { passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength( val result = authRepository.getPasswordStrength(
email = mutableStateFlow.value.emailInput, email = state.emailInput,
password = action.input, password = action.input,
) )
trySendAction(ReceivePasswordStrengthResult(result)) trySendAction(ReceivePasswordStrengthResult(result))
@ -264,7 +264,7 @@ class CreateAccountViewModel @Inject constructor(
} }
private fun handleSubmitClick() = when { private fun handleSubmitClick() = when {
mutableStateFlow.value.emailInput.isBlank() -> { state.emailInput.isBlank() -> {
val dialog = BasicDialogState.Shown( val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required message = R.string.validation_field_required
@ -273,7 +273,7 @@ class CreateAccountViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) } mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) }
} }
!mutableStateFlow.value.emailInput.isValidEmail() -> { !state.emailInput.isValidEmail() -> {
val dialog = BasicDialogState.Shown( val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_email.asText(), message = R.string.invalid_email.asText(),
@ -281,7 +281,7 @@ class CreateAccountViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) } mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) }
} }
mutableStateFlow.value.passwordInput.length < MIN_PASSWORD_LENGTH -> { state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
val dialog = BasicDialogState.Shown( val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_length_val_message_x.asText(MIN_PASSWORD_LENGTH), message = R.string.master_password_length_val_message_x.asText(MIN_PASSWORD_LENGTH),
@ -289,7 +289,7 @@ class CreateAccountViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) } mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) }
} }
mutableStateFlow.value.passwordInput != mutableStateFlow.value.confirmPasswordInput -> { state.passwordInput != state.confirmPasswordInput -> {
val dialog = BasicDialogState.Shown( val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.master_password_confirmation_val_message.asText(), message = R.string.master_password_confirmation_val_message.asText(),
@ -297,7 +297,7 @@ class CreateAccountViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) } mutableStateFlow.update { it.copy(dialog = CreateAccountDialog.Error(dialog)) }
} }
!mutableStateFlow.value.isAcceptPoliciesToggled -> { !state.isAcceptPoliciesToggled -> {
val dialog = BasicDialogState.Shown( val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.accept_policies_error.asText(), message = R.string.accept_policies_error.asText(),
@ -307,7 +307,7 @@ class CreateAccountViewModel @Inject constructor(
else -> { else -> {
submitRegisterAccountRequest( submitRegisterAccountRequest(
shouldCheckForDataBreaches = mutableStateFlow.value.isCheckDataBreachesToggled, shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
captchaToken = null, captchaToken = null,
) )
} }
@ -327,9 +327,9 @@ class CreateAccountViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
val result = authRepository.register( val result = authRepository.register(
shouldCheckDataBreaches = shouldCheckForDataBreaches, shouldCheckDataBreaches = shouldCheckForDataBreaches,
email = mutableStateFlow.value.emailInput, email = state.emailInput,
masterPassword = mutableStateFlow.value.passwordInput, masterPassword = state.passwordInput,
masterPasswordHint = mutableStateFlow.value.passwordHintInput.ifBlank { null }, masterPasswordHint = state.passwordHintInput.ifBlank { null },
captchaToken = captchaToken, captchaToken = captchaToken,
) )
sendAction( sendAction(

View file

@ -72,8 +72,6 @@ class EnvironmentViewModel @Inject constructor(
} }
private fun handleSaveClickAction() { private fun handleSaveClickAction() {
val state = mutableStateFlow.value
val urlsAreAllNullOrValid = listOf( val urlsAreAllNullOrValid = listOf(
state.serverUrl, state.serverUrl,
state.webVaultServerUrl, state.webVaultServerUrl,

View file

@ -128,7 +128,7 @@ class LandingViewModel @Inject constructor(
} }
private fun handleContinueButtonClicked() { private fun handleContinueButtonClicked() {
if (!mutableStateFlow.value.emailInput.isValidEmail()) { if (!state.emailInput.isValidEmail()) {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
dialog = LandingState.DialogState.Error( dialog = LandingState.DialogState.Error(
@ -150,8 +150,8 @@ class LandingViewModel @Inject constructor(
return return
} }
val email = mutableStateFlow.value.emailInput val email = state.emailInput
val isRememberMeEnabled = mutableStateFlow.value.isRememberMeEnabled val isRememberMeEnabled = state.isRememberMeEnabled
// Update the remembered email address // Update the remembered email address
authRepository.rememberedEmailAddress = email.takeUnless { !isRememberMeEnabled } authRepository.rememberedEmailAddress = email.takeUnless { !isRememberMeEnabled }

View file

@ -232,9 +232,9 @@ class LoginViewModel @Inject constructor(
} }
viewModelScope.launch { viewModelScope.launch {
val result = authRepository.login( val result = authRepository.login(
email = mutableStateFlow.value.emailAddress, email = state.emailAddress,
password = mutableStateFlow.value.passwordInput, password = state.passwordInput,
captchaToken = mutableStateFlow.value.captchaToken, captchaToken = state.captchaToken,
) )
sendAction( sendAction(
LoginAction.Internal.ReceiveLoginResult( LoginAction.Internal.ReceiveLoginResult(
@ -245,7 +245,7 @@ class LoginViewModel @Inject constructor(
} }
private fun handleMasterPasswordHintClicked() { private fun handleMasterPasswordHintClicked() {
val email = mutableStateFlow.value.emailAddress val email = state.emailAddress
sendEvent(LoginEvent.NavigateToMasterPasswordHint(email)) sendEvent(LoginEvent.NavigateToMasterPasswordHint(email))
} }
@ -254,7 +254,7 @@ class LoginViewModel @Inject constructor(
} }
private fun handleSingleSignOnClicked() { private fun handleSingleSignOnClicked() {
val email = mutableStateFlow.value.emailAddress val email = state.emailAddress
sendEvent(LoginEvent.NavigateToEnterpriseSignOn(email)) sendEvent(LoginEvent.NavigateToEnterpriseSignOn(email))
} }

View file

@ -160,13 +160,13 @@ class VaultUnlockViewModel @Inject constructor(
val vaultUnlockResult = when (state.vaultUnlockType) { val vaultUnlockResult = when (state.vaultUnlockType) {
VaultUnlockType.MASTER_PASSWORD -> { VaultUnlockType.MASTER_PASSWORD -> {
vaultRepo.unlockVaultWithMasterPassword( vaultRepo.unlockVaultWithMasterPassword(
mutableStateFlow.value.input, state.input,
) )
} }
VaultUnlockType.PIN -> { VaultUnlockType.PIN -> {
vaultRepo.unlockVaultWithPin( vaultRepo.unlockVaultWithPin(
mutableStateFlow.value.input, state.input,
) )
} }
} }

View file

@ -97,9 +97,9 @@ class LoginApprovalViewModel @Inject constructor(
trySendAction( trySendAction(
LoginApprovalAction.Internal.ApproveRequestResultReceive( LoginApprovalAction.Internal.ApproveRequestResultReceive(
result = authRepository.updateAuthRequest( result = authRepository.updateAuthRequest(
requestId = mutableStateFlow.value.requestId, requestId = state.requestId,
masterPasswordHash = mutableStateFlow.value.masterPasswordHash, masterPasswordHash = state.masterPasswordHash,
publicKey = mutableStateFlow.value.publicKey, publicKey = state.publicKey,
isApproved = true, isApproved = true,
), ),
), ),
@ -116,9 +116,9 @@ class LoginApprovalViewModel @Inject constructor(
trySendAction( trySendAction(
LoginApprovalAction.Internal.DeclineRequestResultReceive( LoginApprovalAction.Internal.DeclineRequestResultReceive(
result = authRepository.updateAuthRequest( result = authRepository.updateAuthRequest(
requestId = mutableStateFlow.value.requestId, requestId = state.requestId,
masterPasswordHash = mutableStateFlow.value.masterPasswordHash, masterPasswordHash = state.masterPasswordHash,
publicKey = mutableStateFlow.value.publicKey, publicKey = state.publicKey,
isApproved = false, isApproved = false,
), ),
), ),

View file

@ -76,7 +76,7 @@ class PendingRequestsViewModel @Inject constructor(
viewState = PendingRequestsState.ViewState.Loading, viewState = PendingRequestsState.ViewState.Loading,
) )
} }
mutableStateFlow.value.authRequests.forEach { request -> state.authRequests.forEach { request ->
authRepository.updateAuthRequest( authRepository.updateAuthRequest(
requestId = request.id, requestId = request.id,
masterPasswordHash = request.masterPasswordHash, masterPasswordHash = request.masterPasswordHash,

View file

@ -121,7 +121,7 @@ class ExportVaultViewModel @Inject constructor(
@Suppress("ReturnCount") @Suppress("ReturnCount")
private fun handleConfirmExportVaultClicked() { private fun handleConfirmExportVaultClicked() {
// Display an error alert if the user hasn't entered a password. // Display an error alert if the user hasn't entered a password.
if (mutableStateFlow.value.passwordInput.isBlank()) { if (state.passwordInput.isBlank()) {
updateStateWithError( updateStateWithError(
message = R.string.validation_field_required.asText( message = R.string.validation_field_required.asText(
R.string.master_password.asText(), R.string.master_password.asText(),
@ -149,7 +149,7 @@ class ExportVaultViewModel @Inject constructor(
sendAction( sendAction(
ExportVaultAction.Internal.ReceiveValidatePasswordResult( ExportVaultAction.Internal.ReceiveValidatePasswordResult(
result = authRepository.validatePassword( result = authRepository.validatePassword(
password = mutableStateFlow.value.passwordInput, password = state.passwordInput,
), ),
), ),
) )

View file

@ -550,7 +550,7 @@ class GeneratorViewModel @Inject constructor(
private fun handleRegenerationClick() { private fun handleRegenerationClick() {
// Go through the update process with the current state to trigger a // Go through the update process with the current state to trigger a
// regeneration of the generated text for the same state. // regeneration of the generated text for the same state.
updateGeneratorMainType(forceRegeneration = true) { mutableStateFlow.value.selectedType } updateGeneratorMainType(forceRegeneration = true) { state.selectedType }
} }
private fun handleCopyClick() { private fun handleCopyClick() {
@ -1294,7 +1294,7 @@ class GeneratorViewModel @Inject constructor(
forceRegeneration: Boolean = false, forceRegeneration: Boolean = false,
crossinline block: (GeneratorState.MainType) -> GeneratorState.MainType?, crossinline block: (GeneratorState.MainType) -> GeneratorState.MainType?,
) { ) {
val currentSelectedType = mutableStateFlow.value.selectedType val currentSelectedType = state.selectedType
val updatedMainType = block(currentSelectedType) ?: return val updatedMainType = block(currentSelectedType) ?: return
mutableStateFlow.update { it.copy(selectedType = updatedMainType) } mutableStateFlow.update { it.copy(selectedType = updatedMainType) }

View file

@ -434,7 +434,7 @@ class VaultAddEditViewModel @Inject constructor(
when (action.customFieldAction) { when (action.customFieldAction) {
CustomFieldAction.MOVE_UP -> { CustomFieldAction.MOVE_UP -> {
val items = val items =
(mutableStateFlow.value.viewState as VaultAddEditState.ViewState.Content) (state.viewState as VaultAddEditState.ViewState.Content)
.common .common
.customFieldData .customFieldData
.toMutableList() .toMutableList()
@ -455,7 +455,7 @@ class VaultAddEditViewModel @Inject constructor(
CustomFieldAction.MOVE_DOWN -> { CustomFieldAction.MOVE_DOWN -> {
val items = val items =
(mutableStateFlow.value.viewState as VaultAddEditState.ViewState.Content) (state.viewState as VaultAddEditState.ViewState.Content)
.common .common
.customFieldData .customFieldData
.toMutableList() .toMutableList()

View file

@ -42,75 +42,67 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `CurrentPasswordInputChanged should update the current password input in the state`() = fun `CurrentPasswordInputChanged should update the current password input in the state`() {
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
assertEquals(
DEFAULT_STATE.copy(
currentPasswordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
}
@Test
fun `SubmitClicked with blank password shows error alert`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ResetPasswordState.DialogState.Error( currentPasswordInput = "Test123",
title = null, ),
message = R.string.validation_field_required viewModel.stateFlow.value,
.asText(R.string.master_password.asText()), )
),
),
viewModel.stateFlow.value,
)
// Dismiss the alert.
viewModel.trySendAction(ResetPasswordAction.DialogDismiss)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
}
} }
@Test @Test
fun `SubmitClicked with invalid password shows error alert for weak password reason`() = fun `SubmitClicked with blank password shows error alert`() {
runTest { val viewModel = createViewModel()
val password = "Test123" viewModel.trySendAction(ResetPasswordAction.SubmitClick)
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
} returns false
val viewModel = createViewModel() assertEquals(
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password)) DEFAULT_STATE.copy(
viewModel.eventFlow.test { dialogState = ResetPasswordState.DialogState.Error(
viewModel.trySendAction(ResetPasswordAction.SubmitClick) title = null,
message = R.string.validation_field_required
.asText(R.string.master_password.asText()),
),
),
viewModel.stateFlow.value,
)
assertEquals( // Dismiss the alert.
DEFAULT_STATE.copy( viewModel.trySendAction(ResetPasswordAction.DialogDismiss)
dialogState = ResetPasswordState.DialogState.Error( assertEquals(
title = R.string.master_password_policy_validation_title.asText(), DEFAULT_STATE,
message = R.string.master_password_policy_validation_message.asText(), viewModel.stateFlow.value,
), )
passwordInput = password, }
),
viewModel.stateFlow.value,
)
}
}
@Test @Test
fun `SubmitClicked with invalid password shows error alert for admin reset reason`() = runTest { fun `SubmitClicked with invalid password shows error alert for weak password reason`() {
val password = "Test123"
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
} returns false
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = ResetPasswordState.DialogState.Error(
title = R.string.master_password_policy_validation_title.asText(),
message = R.string.master_password_policy_validation_message.asText(),
),
passwordInput = password,
),
viewModel.stateFlow.value,
)
}
@Test
fun `SubmitClicked with invalid password shows error alert for admin reset reason`() {
val password = "Test123" val password = "Test123"
every { every {
authRepository.passwordResetReason authRepository.passwordResetReason
@ -118,26 +110,24 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET, resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
dialogState = ResetPasswordState.DialogState.Error( dialogState = ResetPasswordState.DialogState.Error(
title = null, title = null,
message = R.string.master_password_length_val_message_x message = R.string.master_password_length_val_message_x
.asText(MIN_PASSWORD_LENGTH), .asText(MIN_PASSWORD_LENGTH),
),
passwordInput = password,
), ),
viewModel.stateFlow.value, passwordInput = password,
) ),
} viewModel.stateFlow.value,
)
} }
@Test @Test
fun `SubmitClicked with non-matching retyped password shows error alert`() = runTest { fun `SubmitClicked with non-matching retyped password shows error alert`() {
val password = "Test123" val password = "Test123"
coEvery { coEvery {
authRepository.validatePasswordAgainstPolicies(password) authRepository.validatePasswordAgainstPolicies(password)
@ -146,24 +136,22 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ResetPasswordState.DialogState.Error( dialogState = ResetPasswordState.DialogState.Error(
title = null, title = null,
message = R.string.master_password_confirmation_val_message.asText(), message = R.string.master_password_confirmation_val_message.asText(),
),
passwordInput = password,
), ),
viewModel.stateFlow.value, passwordInput = password,
) ),
} viewModel.stateFlow.value,
)
} }
@Test @Test
fun `SubmitClicked with error for validating current password shows error alert`() = runTest { fun `SubmitClicked with error for validating current password shows error alert`() {
val currentPassword = "CurrentTest123" val currentPassword = "CurrentTest123"
val password = "Test123" val password = "Test123"
coEvery { coEvery {
@ -178,26 +166,24 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ResetPasswordState.DialogState.Error( dialogState = ResetPasswordState.DialogState.Error(
title = null, title = null,
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
),
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
), ),
viewModel.stateFlow.value, currentPasswordInput = currentPassword,
) passwordInput = password,
} retypePasswordInput = password,
),
viewModel.stateFlow.value,
)
} }
@Test @Test
fun `SubmitClicked with invalid current password shows alert`() = runTest { fun `SubmitClicked with invalid current password shows alert`() {
val currentPassword = "CurrentTest123" val currentPassword = "CurrentTest123"
val password = "Test123" val password = "Test123"
coEvery { coEvery {
@ -212,22 +198,20 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password)) viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.SubmitClick)
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ResetPasswordState.DialogState.Error( dialogState = ResetPasswordState.DialogState.Error(
title = null, title = null,
message = R.string.invalid_master_password.asText(), message = R.string.invalid_master_password.asText(),
),
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
), ),
viewModel.stateFlow.value, currentPasswordInput = currentPassword,
) passwordInput = password,
} retypePasswordInput = password,
),
viewModel.stateFlow.value,
)
} }
@Test @Test
@ -289,49 +273,42 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `PasswordInputChanged should update the password input in the state`() = runTest { fun `PasswordInputChanged should update the password input in the state`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
passwordInput = "Test123", passwordInput = "Test123",
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
}
} }
@Test @Test
fun `RetypePasswordInputChanged should update the retype password input in the state`() = fun `RetypePasswordInputChanged should update the retype password input in the state`() {
runTest { val viewModel = createViewModel()
val viewModel = createViewModel() viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
viewModel.eventFlow.test {
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
retypePasswordInput = "Test123", retypePasswordInput = "Test123",
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
}
@Test @Test
fun `PasswordHintInputChanged should update the password hint input in the state`() = runTest { fun `PasswordHintInputChanged should update the password hint input in the state`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
passwordHintInput = "Test123", passwordHintInput = "Test123",
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
}
} }
private fun createViewModel(): ResetPasswordViewModel = private fun createViewModel(): ResetPasswordViewModel =

View file

@ -162,24 +162,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
runTest { runTest {
val input = "123456" val input = "123456"
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(input))
assertEquals(
DEFAULT_STATE.copy(
codeInput = input,
isContinueButtonEnabled = true,
),
viewModel.stateFlow.value,
)
}
}
@Test
fun `CodeInputChanged should update input and disable button if code is blank`() = runTest {
val input = "123456"
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Set it to true.
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(input)) viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(input))
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
@ -188,17 +170,31 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
// Set it to false.
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(""))
assertEquals(
DEFAULT_STATE.copy(
codeInput = "",
isContinueButtonEnabled = false,
),
viewModel.stateFlow.value,
)
} }
@Test
fun `CodeInputChanged should update input and disable button if code is blank`() {
val input = "123456"
val viewModel = createViewModel()
// Set it to true.
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(input))
assertEquals(
DEFAULT_STATE.copy(
codeInput = input,
isContinueButtonEnabled = true,
),
viewModel.stateFlow.value,
)
// Set it to false.
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged(""))
assertEquals(
DEFAULT_STATE.copy(
codeInput = "",
isContinueButtonEnabled = false,
),
viewModel.stateFlow.value,
)
} }
@Test @Test
@ -415,17 +411,15 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `RememberMeToggle should update the state`() = runTest { fun `RememberMeToggle should update the state`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(TwoFactorLoginAction.RememberMeToggle(true))
viewModel.trySendAction(TwoFactorLoginAction.RememberMeToggle(true)) assertEquals(
assertEquals( DEFAULT_STATE.copy(
DEFAULT_STATE.copy( isRememberMeEnabled = true,
isRememberMeEnabled = true, ),
), viewModel.stateFlow.value,
viewModel.stateFlow.value, )
)
}
} }
@Test @Test
@ -534,21 +528,19 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `SelectAuthMethod with other method should update the state`() = runTest { fun `SelectAuthMethod with other method should update the state`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(
viewModel.trySendAction( TwoFactorLoginAction.SelectAuthMethod(
TwoFactorLoginAction.SelectAuthMethod( TwoFactorAuthMethod.AUTHENTICATOR_APP,
TwoFactorAuthMethod.AUTHENTICATOR_APP, ),
), )
) assertEquals(
assertEquals( DEFAULT_STATE.copy(
DEFAULT_STATE.copy( authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP, ),
), viewModel.stateFlow.value,
viewModel.stateFlow.value, )
)
}
} }
@Test @Test
@ -569,8 +561,8 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
), ),
awaitItem(), awaitItem(),
) )
}
} }
}
@Test @Test
@Suppress("MaxLineLength") @Suppress("MaxLineLength")

View file

@ -86,137 +86,130 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ConfirmExportVaultClicked correct password should call exportVaultDataToString`() = fun `ConfirmExportVaultClicked correct password should call exportVaultDataToString`() {
runTest { val password = "password"
val password = "password" coEvery {
coEvery { authRepository.validatePassword(
authRepository.validatePassword( password = password,
password = password, )
) } returns ValidatePasswordResult.Success(isValid = true)
} returns ValidatePasswordResult.Success(isValid = true)
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
coVerify { coVerify {
vaultRepository.exportVaultDataToString(any()) vaultRepository.exportVaultDataToString(any())
}
}
} }
}
@Test @Test
fun `ConfirmExportVaultClicked blank password should show an error`() = runTest { fun `ConfirmExportVaultClicked blank password should show an error`() {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) assertEquals(
assertEquals( DEFAULT_STATE.copy(
DEFAULT_STATE.copy( dialogState = ExportVaultState.DialogState.Error(
dialogState = ExportVaultState.DialogState.Error( title = R.string.an_error_has_occurred.asText(),
title = R.string.an_error_has_occurred.asText(), message = R.string.validation_field_required.asText(
message = R.string.validation_field_required.asText( R.string.master_password.asText(),
R.string.master_password.asText(),
),
), ),
), ),
viewModel.stateFlow.value, ),
) viewModel.stateFlow.value,
)
viewModel.trySendAction(ExportVaultAction.DialogDismiss) viewModel.trySendAction(ExportVaultAction.DialogDismiss)
assertEquals( assertEquals(
DEFAULT_STATE, DEFAULT_STATE,
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `ConfirmExportVaultClicked blank file password should show an error when export type is JSON_ENCRYPTED`() = fun `ConfirmExportVaultClicked blank file password should show an error when export type is JSON_ENCRYPTED`() {
runTest { val password = "password"
val password = "password" val viewModel = createViewModel()
val viewModel = createViewModel() viewModel.trySendAction(
viewModel.trySendAction( ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.JSON_ENCRYPTED),
ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.JSON_ENCRYPTED), )
) viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password)) viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) assertEquals(
assertEquals( DEFAULT_STATE.copy(
DEFAULT_STATE.copy( dialogState = ExportVaultState.DialogState.Error(
dialogState = ExportVaultState.DialogState.Error( title = R.string.an_error_has_occurred.asText(),
title = R.string.an_error_has_occurred.asText(), message = R.string.validation_field_required.asText(
message = R.string.validation_field_required.asText( R.string.file_password.asText(),
R.string.file_password.asText(),
),
), ),
exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
passwordInput = password,
), ),
viewModel.stateFlow.value, exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
) passwordInput = password,
),
viewModel.stateFlow.value,
)
viewModel.trySendAction(ExportVaultAction.DialogDismiss) viewModel.trySendAction(ExportVaultAction.DialogDismiss)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
exportFormat = ExportVaultFormat.JSON_ENCRYPTED, exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
passwordInput = password, passwordInput = password,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `ConfirmExportVaultClicked blank confirm file password should show an error when export type is JSON_ENCRYPTED`() = fun `ConfirmExportVaultClicked blank confirm file password should show an error when export type is JSON_ENCRYPTED`() {
runTest { val password = "password"
val password = "password" coEvery {
coEvery { authRepository.getPasswordStrength(
authRepository.getPasswordStrength( email = EMAIL_ADDRESS,
email = EMAIL_ADDRESS, password = password,
password = password,
)
} returns PasswordStrengthResult.Success(
passwordStrength = PasswordStrength.LEVEL_4,
) )
val viewModel = createViewModel() } returns PasswordStrengthResult.Success(
viewModel.trySendAction( passwordStrength = PasswordStrength.LEVEL_4,
ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.JSON_ENCRYPTED), )
) val viewModel = createViewModel()
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password)) viewModel.trySendAction(
viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange(password)) ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.JSON_ENCRYPTED),
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) )
assertEquals( viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
DEFAULT_STATE.copy( viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange(password))
dialogState = ExportVaultState.DialogState.Error( viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
title = R.string.an_error_has_occurred.asText(), assertEquals(
message = R.string.validation_field_required.asText( DEFAULT_STATE.copy(
R.string.confirm_file_password.asText(), dialogState = ExportVaultState.DialogState.Error(
), title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required.asText(
R.string.confirm_file_password.asText(),
), ),
exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
filePasswordInput = password,
passwordInput = password,
passwordStrengthState = PasswordStrengthState.STRONG,
), ),
viewModel.stateFlow.value, exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
) filePasswordInput = password,
passwordInput = password,
passwordStrengthState = PasswordStrengthState.STRONG,
),
viewModel.stateFlow.value,
)
viewModel.trySendAction(ExportVaultAction.DialogDismiss) viewModel.trySendAction(ExportVaultAction.DialogDismiss)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
exportFormat = ExportVaultFormat.JSON_ENCRYPTED, exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
filePasswordInput = password, filePasswordInput = password,
passwordInput = password, passwordInput = password,
passwordStrengthState = PasswordStrengthState.STRONG, passwordStrengthState = PasswordStrengthState.STRONG,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
@Test @Test
fun `ConfirmExportVaultClicked invalid password should show an error`() = runTest { fun `ConfirmExportVaultClicked invalid password should show an error`() {
val password = "password" val password = "password"
coEvery { coEvery {
authRepository.validatePassword( authRepository.validatePassword(
@ -225,21 +218,19 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} returns ValidatePasswordResult.Success(isValid = false) } returns ValidatePasswordResult.Success(isValid = false)
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ExportVaultState.DialogState.Error( dialogState = ExportVaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_master_password.asText(), message = R.string.invalid_master_password.asText(),
),
passwordInput = password,
), ),
viewModel.stateFlow.value, passwordInput = password,
) ),
} viewModel.stateFlow.value,
)
coVerify { coVerify {
authRepository.validatePassword( authRepository.validatePassword(
password = password, password = password,
@ -248,7 +239,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ConfirmExportVaultClicked error checking password should show an error`() = runTest { fun `ConfirmExportVaultClicked error checking password should show an error`() {
val password = "password" val password = "password"
coEvery { coEvery {
authRepository.validatePassword( authRepository.validatePassword(
@ -257,40 +248,35 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} returns ValidatePasswordResult.Error } returns ValidatePasswordResult.Error
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(password))
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ExportVaultState.DialogState.Error( dialogState = ExportVaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
),
passwordInput = password,
), ),
viewModel.stateFlow.value, passwordInput = password,
) ),
} viewModel.stateFlow.value,
)
} }
@Test @Test
fun `ExportFormatOptionSelect should update the selected export format in the state`() = fun `ExportFormatOptionSelect should update the selected export format in the state`() {
runTest { val viewModel = createViewModel()
val viewModel = createViewModel() viewModel.trySendAction(
viewModel.eventFlow.test { ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.CSV),
viewModel.trySendAction( )
ExportVaultAction.ExportFormatOptionSelect(ExportVaultFormat.CSV),
)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
exportFormat = ExportVaultFormat.CSV, exportFormat = ExportVaultFormat.CSV,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
}
@Test @Test
fun `ConfirmFilePasswordInputChanged should update the confirm password input in the state`() { fun `ConfirmFilePasswordInputChanged should update the confirm password input in the state`() {
@ -348,26 +334,25 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() = fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() {
runTest { val viewModel = createViewModel()
val viewModel = createViewModel()
viewModel.trySendAction( viewModel.trySendAction(
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult( ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
result = ExportVaultDataResult.Error, result = ExportVaultDataResult.Error,
), ),
) )
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
dialogState = ExportVaultState.DialogState.Error( dialogState = ExportVaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(), title = R.string.an_error_has_occurred.asText(),
message = R.string.export_vault_failure.asText(), message = R.string.export_vault_failure.asText(),
),
), ),
viewModel.stateFlow.value, ),
) viewModel.stateFlow.value,
} )
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
@ -493,37 +478,36 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ExportLocationReceive should update state to error if saving the data fails`() = fun `ExportLocationReceive should update state to error if saving the data fails`() {
runTest { val exportData = "TestExportVaultData"
val exportData = "TestExportVaultData" val viewModel = createViewModel(
val viewModel = createViewModel( DEFAULT_STATE.copy(
DEFAULT_STATE.copy( exportData = exportData,
exportData = exportData, ),
), )
) val uri = mockk<Uri>()
val uri = mockk<Uri>() coEvery {
coEvery { fileManager.stringToUri(fileUri = any(), dataString = exportData)
fileManager.stringToUri(fileUri = any(), dataString = exportData) } returns false
} returns false
viewModel.trySendAction(ExportVaultAction.ExportLocationReceive(fileUri = uri)) viewModel.trySendAction(ExportVaultAction.ExportLocationReceive(fileUri = uri))
coVerify { coVerify {
fileManager.stringToUri(fileUri = any(), dataString = exportData) fileManager.stringToUri(fileUri = any(), dataString = exportData)
}
assertEquals(
DEFAULT_STATE.copy(
exportData = exportData,
dialogState = ExportVaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.export_vault_failure.asText(),
),
),
viewModel.stateFlow.value,
)
} }
assertEquals(
DEFAULT_STATE.copy(
exportData = exportData,
dialogState = ExportVaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.export_vault_failure.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test @Test
fun `ExportLocationReceive should emit ShowToast on success`() = runTest { fun `ExportLocationReceive should emit ShowToast on success`() = runTest {
val exportData = "TestExportVaultData" val exportData = "TestExportVaultData"