PM-10692 pass a generated password back to the complete registration … (#3806)

This commit is contained in:
Dave Severns 2024-08-26 08:56:28 -04:00 committed by GitHub
parent 666c165b6f
commit 76a3265bbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 229 additions and 92 deletions

View file

@ -10,6 +10,7 @@ import com.x8bit.bitwarden.ui.auth.feature.checkemail.checkEmailDestination
import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination import com.x8bit.bitwarden.ui.auth.feature.completeregistration.completeRegistrationDestination
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.popUpToCompleteRegistration
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
@ -184,6 +185,9 @@ fun NavGraphBuilder.authGraph(
masterPasswordGeneratorDestination( masterPasswordGeneratorDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToPreventLockout = { navController.navigateToPreventAccountLockout() }, onNavigateToPreventLockout = { navController.navigateToPreventAccountLockout() },
onNavigateBackWithPassword = {
navController.popUpToCompleteRegistration()
},
) )
} }
} }

View file

@ -72,3 +72,10 @@ fun NavGraphBuilder.completeRegistrationDestination(
) )
} }
} }
/**
* Pop up to the complete registration screen.
*/
fun NavController.popUpToCompleteRegistration() {
popBackStack(route = COMPLETE_REGISTRATION_ROUTE, inclusive = false)
}

View file

@ -216,7 +216,8 @@ private fun CompleteRegistrationContent(
callToActionText = stringResource(id = R.string.learn_more), callToActionText = stringResource(id = R.string.learn_more),
onCardClicked = handler.onMakeStrongPassword, onCardClicked = handler.onMakeStrongPassword,
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.standardHorizontalMargin(),
) )
} else { } else {
LegacyHeaderContent( LegacyHeaderContent(

View file

@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
@ -28,6 +30,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -48,12 +51,12 @@ private const val MIN_PASSWORD_LENGTH = 12
class CompleteRegistrationViewModel @Inject constructor( class CompleteRegistrationViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
featureFlagManager: FeatureFlagManager, featureFlagManager: FeatureFlagManager,
generatorRepository: GeneratorRepository,
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository, private val environmentRepository: EnvironmentRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager, private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>( ) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE] ?: run {
?: run {
val args = CompleteRegistrationArgs(savedStateHandle) val args = CompleteRegistrationArgs(savedStateHandle)
CompleteRegistrationState( CompleteRegistrationState(
userEmail = args.emailAddress, userEmail = args.emailAddress,
@ -91,6 +94,15 @@ class CompleteRegistrationViewModel @Inject constructor(
} }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
generatorRepository
.generatorResultFlow
.filterIsInstance<GeneratorResult.Password>()
.map {
Internal.GeneratedPasswordResult(generatedPassword = it.password)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
@VisibleForTesting @VisibleForTesting
@ -124,9 +136,25 @@ class CompleteRegistrationViewModel @Inject constructor(
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick() CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
is Internal.UpdateOnboardingFeatureState -> handleUpdateOnboardingFeatureState(action) is Internal.UpdateOnboardingFeatureState -> handleUpdateOnboardingFeatureState(action)
is Internal.GeneratedPasswordResult -> handleGeneratedPasswordResult(
action,
)
} }
} }
private fun handleGeneratedPasswordResult(
action: Internal.GeneratedPasswordResult,
) {
val password = action.generatedPassword
mutableStateFlow.update {
it.copy(
passwordInput = password,
confirmPasswordInput = password,
)
}
checkPasswordStrength(input = password)
}
private fun verifyEmailAddress() { private fun verifyEmailAddress() {
if (!state.fromEmail) { if (!state.fromEmail) {
return return
@ -262,21 +290,7 @@ class CompleteRegistrationViewModel @Inject constructor(
private fun handlePasswordInputChanged(action: PasswordInputChange) { private fun handlePasswordInputChanged(action: PasswordInputChange) {
// Update input: // Update input:
mutableStateFlow.update { it.copy(passwordInput = action.input) } mutableStateFlow.update { it.copy(passwordInput = action.input) }
// Update password strength: checkPasswordStrength(action.input)
passwordStrengthJob.cancel()
if (action.input.isEmpty()) {
mutableStateFlow.update {
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
}
} else {
passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength(
email = state.userEmail,
password = action.input,
)
trySendAction(ReceivePasswordStrengthResult(result))
}
}
} }
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) { private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
@ -367,6 +381,24 @@ class CompleteRegistrationViewModel @Inject constructor(
) )
} }
} }
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(
email = state.userEmail,
password = input,
)
trySendAction(ReceivePasswordStrengthResult(result))
}
}
}
} }
/** /**
@ -564,5 +596,10 @@ sealed class CompleteRegistrationAction {
* Indicate on boarding feature state has been updated. * Indicate on boarding feature state has been updated.
*/ */
data class UpdateOnboardingFeatureState(val newValue: Boolean) : Internal() data class UpdateOnboardingFeatureState(val newValue: Boolean) : Internal()
/**
* Indicates a generated password has been received.
*/
data class GeneratedPasswordResult(val generatedPassword: String) : Internal()
} }
} }

View file

@ -20,6 +20,7 @@ fun NavController.navigateToMasterPasswordGenerator(navOptions: NavOptions? = nu
fun NavGraphBuilder.masterPasswordGeneratorDestination( fun NavGraphBuilder.masterPasswordGeneratorDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToPreventLockout: () -> Unit, onNavigateToPreventLockout: () -> Unit,
onNavigateBackWithPassword: () -> Unit,
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(
route = MASTER_PASSWORD_GENERATOR, route = MASTER_PASSWORD_GENERATOR,
@ -27,6 +28,7 @@ fun NavGraphBuilder.masterPasswordGeneratorDestination(
MasterPasswordGeneratorScreen( MasterPasswordGeneratorScreen(
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
onNavigateToPreventLockout = onNavigateToPreventLockout, onNavigateToPreventLockout = onNavigateToPreventLockout,
onNavigateBackWithPassword = onNavigateBackWithPassword,
) )
} }
} }

View file

@ -53,6 +53,7 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
fun MasterPasswordGeneratorScreen( fun MasterPasswordGeneratorScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToPreventLockout: () -> Unit, onNavigateToPreventLockout: () -> Unit,
onNavigateBackWithPassword: () -> Unit,
viewModel: MasterPasswordGeneratorViewModel = hiltViewModel(), viewModel: MasterPasswordGeneratorViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -71,6 +72,10 @@ fun MasterPasswordGeneratorScreen(
duration = SnackbarDuration.Short, duration = SnackbarDuration.Short,
) )
} }
is MasterPasswordGeneratorEvent.NavigateBackToRegistration -> {
onNavigateBackWithPassword()
}
} }
} }

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -73,7 +74,8 @@ class MasterPasswordGeneratorViewModel @Inject constructor(
private fun handleBackAction() = sendEvent(MasterPasswordGeneratorEvent.NavigateBack) private fun handleBackAction() = sendEvent(MasterPasswordGeneratorEvent.NavigateBack)
private fun handleSavePasswordAction() { private fun handleSavePasswordAction() {
// TODO [PM-10692](https://bitwarden.atlassian.net/browse/PM-10692) generatorRepository.emitGeneratorResult(GeneratorResult.Password(state.generatedPassword))
sendEvent(MasterPasswordGeneratorEvent.NavigateBackToRegistration)
} }
private fun handlePreventLockoutAction() = private fun handlePreventLockoutAction() =
@ -163,6 +165,11 @@ sealed class MasterPasswordGeneratorEvent {
* Show a Snackbar message. * Show a Snackbar message.
*/ */
data class ShowSnackbar(val text: Text) : MasterPasswordGeneratorEvent() data class ShowSnackbar(val text: Text) : MasterPasswordGeneratorEvent()
/**
* Navigate back to the complete registration screen.
*/
data object NavigateBackToRegistration : MasterPasswordGeneratorEvent()
} }
/** /**

View file

@ -20,6 +20,9 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
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.model.GeneratorResult
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.BackClick
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult
@ -61,6 +64,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
} }
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
every { generatorResultFlow } returns mutableGeneratorResultFlow
}
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
@ -95,14 +102,14 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
environmentRepository = fakeEnvironmentRepository, environmentRepository = fakeEnvironmentRepository,
specialCircumstanceManager = specialCircumstanceManager, specialCircumstanceManager = specialCircumstanceManager,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
generatorRepository = generatorRepository,
) )
viewModel.onCleared() viewModel.onCleared()
assertTrue(specialCircumstanceManager.specialCircumstance == null) assertTrue(specialCircumstanceManager.specialCircumstance == null)
} }
@Test @Test
fun `Password below 12 chars should have non-valid state`() = fun `Password below 12 chars should have non-valid state`() = runTest {
runTest {
val input = "abcdefghikl" val input = "abcdefghikl"
coEvery { coEvery {
mockAuthRepository.getPasswordStrength(EMAIL, input) mockAuthRepository.getPasswordStrength(EMAIL, input)
@ -114,8 +121,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `Passwords not matching should have non-valid state`() = fun `Passwords not matching should have non-valid state`() = runTest {
runTest {
coEvery { coEvery {
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
} returns PasswordStrengthResult.Error } returns PasswordStrengthResult.Error
@ -344,8 +350,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
) )
} returns RegisterResult.WeakPassword } returns RegisterResult.WeakPassword
} }
val initialState = VALID_INPUT_STATE val initialState = VALID_INPUT_STATE.copy(
.copy(
passwordStrengthState = PasswordStrengthState.WEAK_1, passwordStrengthState = PasswordStrengthState.WEAK_1,
isCheckDataBreachesToggled = true, isCheckDataBreachesToggled = true,
) )
@ -418,6 +423,47 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
coVerify { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) } coVerify { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
} }
@Test
fun `Empty PasswordInputChange update should result in password strength being NONE`() =
runTest {
val viewModel = createCompleteRegistrationViewModel(
completeRegistrationState = DEFAULT_STATE.copy(
passwordInput = PASSWORD,
passwordStrengthState = PasswordStrengthState.STRONG,
),
)
viewModel.trySendAction(PasswordInputChange(""))
val expectedStrengthUpdateState = DEFAULT_STATE.copy(
passwordInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
)
viewModel.stateFlow.test {
assertEquals(expectedStrengthUpdateState, awaitItem())
}
coVerify(exactly = 0) { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
}
@Suppress("MaxLineLength")
@Test
fun `Internal GeneratedPasswordResult update passwordInput and confirmPasswordInput and call getPasswordStrength`() =
runTest {
coEvery {
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
} returns PasswordStrengthResult.Error
val viewModel = createCompleteRegistrationViewModel()
mutableGeneratorResultFlow.emit(GeneratorResult.Password(PASSWORD))
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
passwordInput = PASSWORD,
confirmPasswordInput = PASSWORD,
),
awaitItem(),
)
}
coVerify { mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD) }
}
@Test @Test
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest { fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
val viewModel = createCompleteRegistrationViewModel() val viewModel = createCompleteRegistrationViewModel()
@ -565,8 +611,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `CreateAccountClick with no email not should show dialog`() = fun `CreateAccountClick with no email not should show dialog`() = runTest {
runTest {
coEvery { coEvery {
mockAuthRepository.getPasswordStrength("", PASSWORD) mockAuthRepository.getPasswordStrength("", PASSWORD)
} returns PasswordStrengthResult.Error } returns PasswordStrengthResult.Error
@ -580,8 +625,9 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
dialog = CompleteRegistrationDialog.Error( dialog = CompleteRegistrationDialog.Error(
BasicDialogState.Shown( 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.asText(
.asText(R.string.email_address.asText()), R.string.email_address.asText(),
),
), ),
), ),
) )
@ -594,8 +640,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
private fun createCompleteRegistrationViewModel( private fun createCompleteRegistrationViewModel(
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE, completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
authRepository: AuthRepository = mockAuthRepository, authRepository: AuthRepository = mockAuthRepository,
): CompleteRegistrationViewModel = ): CompleteRegistrationViewModel = CompleteRegistrationViewModel(
CompleteRegistrationViewModel(
savedStateHandle = SavedStateHandle( savedStateHandle = SavedStateHandle(
mapOf( mapOf(
"state" to completeRegistrationState, "state" to completeRegistrationState,
@ -605,6 +650,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
environmentRepository = fakeEnvironmentRepository, environmentRepository = fakeEnvironmentRepository,
specialCircumstanceManager = specialCircumstanceManager, specialCircumstanceManager = specialCircumstanceManager,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
generatorRepository = generatorRepository,
) )
companion object { companion object {

View file

@ -20,6 +20,7 @@ import org.junit.Test
class MasterPasswordGeneratorScreenTest : BaseComposeTest() { class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
private var onNavigateToPreventLockoutCalled = false private var onNavigateToPreventLockoutCalled = false
private var navigateBackWithPasswordCalled = false
private val mutableEventFlow = bufferedMutableSharedFlow<MasterPasswordGeneratorEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<MasterPasswordGeneratorEvent>()
private val mutableStateFlow = MutableStateFlow( private val mutableStateFlow = MutableStateFlow(
value = MasterPasswordGeneratorState(generatedPassword = "-"), value = MasterPasswordGeneratorState(generatedPassword = "-"),
@ -35,6 +36,7 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
MasterPasswordGeneratorScreen( MasterPasswordGeneratorScreen(
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToPreventLockout = { onNavigateToPreventLockoutCalled = true }, onNavigateToPreventLockout = { onNavigateToPreventLockoutCalled = true },
onNavigateBackWithPassword = { navigateBackWithPasswordCalled = true },
viewModel = viewModel, viewModel = viewModel,
) )
} }
@ -42,7 +44,7 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
@Test @Test
fun `Generated password field state should update with ViewModel state`() { fun `Generated password field state should update with ViewModel state`() {
val updatedValue = "soup-r-stronk-pazzwerd" val updatedValue = PASSWORD_INPUT
mutableStateFlow.update { it.copy(generatedPassword = updatedValue) } mutableStateFlow.update { it.copy(generatedPassword = updatedValue) }
composeTestRule composeTestRule
@ -104,4 +106,15 @@ class MasterPasswordGeneratorScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(MasterPasswordGeneratorAction.PreventLockoutClickAction) } verify { viewModel.trySendAction(MasterPasswordGeneratorAction.PreventLockoutClickAction) }
} }
@Test
fun `Verify navigating back with password invokes the lambda`() {
mutableEventFlow.tryEmit(
MasterPasswordGeneratorEvent.NavigateBackToRegistration,
)
assertTrue(navigateBackWithPasswordCalled)
} }
}
private const val PASSWORD_INPUT = "password1234"

View file

@ -124,6 +124,21 @@ class MasterPasswordGeneratorViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `NavigateBackWithPassword event is sent when SavePasswordClickAction is handled`() =
runTest {
val viewModel = createViewModel(
initialState = MasterPasswordGeneratorState(generatedPassword = "saved-pw"),
)
viewModel.eventFlow.test {
viewModel.trySendAction(MasterPasswordGeneratorAction.SavePasswordClickAction)
assertEquals(
MasterPasswordGeneratorEvent.NavigateBackToRegistration,
awaitItem(),
)
}
}
// region helpers // region helpers
private fun createViewModel( private fun createViewModel(