BIT-71: Adding logic for password hint implementation (#748)

This commit is contained in:
Joshua Queen 2024-01-24 11:30:06 -05:00 committed by Álison Fernandes
parent 96201fd34c
commit 5279f1a4ba
14 changed files with 479 additions and 3 deletions

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
@ -17,4 +18,9 @@ interface AccountsApi {
@POST("/accounts/register")
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
@POST("/accounts/password-hint")
suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson,
): Result<Unit>
}

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body for password hint.
*/
@Serializable
data class PasswordHintRequestJson(
@SerialName("email")
val email: String,
)

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models response bodies from password hint response.
*/
@Serializable
sealed class PasswordHintResponseJson {
/**
* The success body of the password hint response
*/
@Serializable
data object Success : PasswordHintResponseJson()
/**
* The error body of an invalid request containing a message.
*/
@Serializable
data class Error(
@SerialName("message")
val errorMessage: String?,
) : PasswordHintResponseJson()
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
@ -23,4 +24,9 @@ interface AccountsService {
* Register a new account to Bitwarden.
*/
suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson>
/**
* Request a password hint.
*/
suspend fun requestPasswordHint(email: String): Result<PasswordHintResponseJson>
}

View file

@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
@ -40,4 +42,20 @@ class AccountsServiceImpl constructor(
json = json,
) ?: throw throwable
}
override suspend fun requestPasswordHint(
email: String,
): Result<PasswordHintResponseJson> =
accountsApi
.passwordHintRequest(PasswordHintRequestJson(email))
.map { PasswordHintResponseJson.Success }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<PasswordHintResponseJson.Error>(
code = 429,
json = json,
)
?: throw throwable
}
}

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
@ -98,6 +99,13 @@ interface AuthRepository : AuthenticatorProvider {
shouldCheckDataBreaches: Boolean,
): RegisterResult
/**
* Attempt to request a password hint.
*/
suspend fun passwordHintRequest(
email: String,
): PasswordHintResult
/**
* Set the value of [captchaTokenResultFlow].
*/

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.Success
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
@ -22,6 +23,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
@ -385,6 +387,20 @@ class AuthRepositoryImpl(
)
}
override suspend fun passwordHintRequest(email: String): PasswordHintResult {
return accountsService.requestPasswordHint(email).fold(
onSuccess = {
when (it) {
is PasswordHintResponseJson.Error -> {
PasswordHintResult.Error(it.errorMessage)
}
PasswordHintResponseJson.Success -> PasswordHintResult.Success
}
},
onFailure = { PasswordHintResult.Error(null) },
)
}
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
mutableCaptchaTokenFlow.tryEmit(tokenResult)
}

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of password hint request.
*/
sealed class PasswordHintResult {
/**
* Password hint request success
*/
data object Success : PasswordHintResult()
/**
* There was an error.
*/
data class Error(val message: String?) : PasswordHintResult()
}

View file

@ -25,10 +25,12 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
/**
* The top level composable for the Login screen.
@ -60,10 +62,18 @@ fun MasterPasswordHintScreen(
)
}
is MasterPasswordHintState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = dialogState.message,
),
)
}
is MasterPasswordHintState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = R.string.password_hint.asText(),
title = dialogState.title ?: R.string.an_error_has_occurred.asText(),
message = dialogState.message,
),
onDismissRequest = remember(viewModel) {

View file

@ -3,12 +3,18 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
@ -19,7 +25,9 @@ private const val KEY_STATE = "state"
*/
@HiltViewModel
class MasterPasswordHintViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val savedStateHandle: SavedStateHandle,
private val networkConnectionManager: NetworkConnectionManager,
) : BaseViewModel<MasterPasswordHintState, MasterPasswordHintEvent, MasterPasswordHintAction>(
initialState = savedStateHandle[KEY_STATE]
?: MasterPasswordHintState(
@ -40,6 +48,9 @@ class MasterPasswordHintViewModel @Inject constructor(
MasterPasswordHintAction.SubmitClick -> handleSubmitClick()
is MasterPasswordHintAction.EmailInputChange -> handleEmailInputUpdated(action)
MasterPasswordHintAction.DismissDialog -> handleDismissDialog()
is MasterPasswordHintAction.Internal.PasswordHintResultReceive -> {
handlePasswordHintResult(action)
}
}
}
@ -49,8 +60,86 @@ class MasterPasswordHintViewModel @Inject constructor(
)
}
@Suppress("LongMethod", "ReturnCount")
private fun handleSubmitClick() {
// TODO (BIT-71): Implement master password hint
val email = stateFlow.value.emailInput
if (!networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update {
it.copy(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
),
)
}
return
}
if (email.isBlank()) {
val errorMessage =
R.string.validation_field_required.asText(R.string.email_address.asText())
mutableStateFlow.update {
it.copy(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = errorMessage,
),
)
}
return
}
if (!email.contains("@")) {
val errorMessage = R.string.invalid_email.asText()
mutableStateFlow.update {
it.copy(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = errorMessage,
),
)
}
return
}
mutableStateFlow.update {
it.copy(
dialog = MasterPasswordHintState.DialogState.Loading(
R.string.submitting.asText(),
),
)
}
viewModelScope.launch {
val result = authRepository.passwordHintRequest(email)
sendAction(MasterPasswordHintAction.Internal.PasswordHintResultReceive(result))
}
}
private fun handlePasswordHintResult(
action: MasterPasswordHintAction.Internal.PasswordHintResultReceive,
) {
when (action.result) {
is PasswordHintResult.Success -> {
mutableStateFlow.update {
it.copy(dialog = MasterPasswordHintState.DialogState.PasswordHintSent)
}
}
is PasswordHintResult.Error -> {
val errorMessage = action.result.message?.asText()
?: R.string.generic_error_message.asText()
mutableStateFlow.update {
it.copy(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = errorMessage,
),
)
}
}
}
}
private fun handleEmailInputUpdated(action: MasterPasswordHintAction.EmailInputChange) {
@ -87,11 +176,20 @@ data class MasterPasswordHintState(
@Parcelize
data object PasswordHintSent : DialogState()
/**
* Represents a loading dialog with the given [message].
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
/**
* Represents an error dialog with the given [message].
*/
@Parcelize
data class Error(
val title: Text? = null,
val message: Text,
) : DialogState()
}
@ -132,4 +230,16 @@ sealed class MasterPasswordHintAction {
* User dismissed the currently displayed dialog.
*/
data object DismissDialog : MasterPasswordHintAction()
/**
* Actions for internal use by the ViewModel.
*/
sealed class Internal : MasterPasswordHintAction() {
/**
* Indicates that the password hint result was received.
*/
data class PasswordHintResultReceive(
val result: PasswordHintResult,
) : Internal()
}
}

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
@ -198,6 +199,18 @@ class AccountsServiceTest : BaseServiceTest() {
assertEquals(Result.success(expectedResponse), service.register(registerRequestBody))
}
@Test
fun `requestPasswordHint success should return Success`() = runTest {
val email = "test@example.com"
val response = MockResponse().setResponseCode(200).setBody("{}")
server.enqueue(response)
val result = service.requestPasswordHint(email)
assertTrue(result.isSuccess)
assertEquals(PasswordHintResponseJson.Success, result.getOrNull())
}
companion object {
private const val EMAIL = "email"
private val registerRequestBody = RegisterRequestJson(

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
@ -32,6 +33,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
@ -998,6 +1000,43 @@ class AuthRepositoryTest {
assertEquals(RegisterResult.Error(errorMessage = "message"), result)
}
@Test
fun `passwordHintRequest with valid email should return Success`() = runTest {
val email = "valid@example.com"
coEvery {
accountsService.requestPasswordHint(email)
} returns Result.success(PasswordHintResponseJson.Success)
val result = repository.passwordHintRequest(email)
assertEquals(PasswordHintResult.Success, result)
}
@Test
fun `passwordHintRequest with error response should return Error`() = runTest {
val email = "error@example.com"
val errorMessage = "Error message"
coEvery {
accountsService.requestPasswordHint(email)
} returns Result.success(PasswordHintResponseJson.Error(errorMessage))
val result = repository.passwordHintRequest(email)
assertEquals(PasswordHintResult.Error(errorMessage), result)
}
@Test
fun `passwordHintRequest with failure should return Error with null message`() = runTest {
val email = "failure@example.com"
coEvery {
accountsService.requestPasswordHint(email)
} returns Result.failure(RuntimeException("Network error"))
val result = repository.passwordHintRequest(email)
assertEquals(PasswordHintResult.Error(null), result)
}
@Test
fun `setCaptchaCallbackToken should change the value of captchaTokenFlow`() = runTest {
repository.captchaTokenResultFlow.test {

View file

@ -1,9 +1,13 @@
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextReplacement
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
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 io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@ -41,6 +45,60 @@ class MasterPasswordHintScreenTest : BaseComposeTest() {
}
}
@Test
fun `should show success dialog when PasswordHintSent state is set`() {
mutableStateFlow.value = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.PasswordHintSent,
emailInput = "test@example.com",
)
composeTestRule
.onNodeWithText("We've sent you an email with your master password hint.")
.assertIsDisplayed()
}
@Test
fun `should show error dialog when Error state is set`() {
val errorMessage = "Error occurred"
mutableStateFlow.value = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(message = errorMessage.asText()),
emailInput = "test@example.com",
)
composeTestRule
.onNodeWithText(errorMessage)
.assertIsDisplayed()
}
@Test
fun `should show loading dialog when Loading state is set`() {
val loadingMessage = "Submitting"
mutableStateFlow.value = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Loading(message = loadingMessage.asText()),
emailInput = "test@example.com",
)
composeTestRule
.onNodeWithText(loadingMessage)
.assertIsDisplayed()
}
@Test
fun `clicking ok in dialog should send DismissDialog action`() {
composeTestRule.assertNoDialogExists()
mutableStateFlow.value = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(message = "".asText()),
emailInput = "test@example.com",
)
composeTestRule
.onNodeWithText("Ok")
.performClick()
verify { viewModel.trySendAction(MasterPasswordHintAction.DismissDialog) }
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(MasterPasswordHintEvent.NavigateBack)

View file

@ -2,13 +2,24 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class MasterPasswordHintViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = mockk()
private val networkConnectionManager: NetworkConnectionManager = mockk()
@Suppress("MaxLineLength")
@Test
fun `initial state should be correct`() = runTest {
@ -18,6 +29,129 @@ class MasterPasswordHintViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `SubmitClick with valid email should show success dialog`() = runTest {
val validEmail = "test@example.com"
every { networkConnectionManager.isNetworkConnected } returns true
coEvery {
authRepository.passwordHintRequest(validEmail)
} returns PasswordHintResult.Success
val viewModel = createViewModel()
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(validEmail))
viewModel.trySendAction(MasterPasswordHintAction.SubmitClick)
viewModel.stateFlow.test {
val expectedSuccessState = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.PasswordHintSent,
emailInput = validEmail,
)
assertEquals(expectedSuccessState, awaitItem())
}
}
@Test
fun `SubmitClick with no network connection should show error dialog`() = runTest {
val email = "test@example.com"
every { networkConnectionManager.isNetworkConnected } returns false
val viewModel = createViewModel()
val expectedErrorState = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
),
emailInput = email,
)
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(email))
viewModel.trySendAction(MasterPasswordHintAction.SubmitClick)
viewModel.stateFlow.test {
assertEquals(expectedErrorState, awaitItem())
}
}
@Test
fun `SubmitClick with empty email field should show error dialog`() = runTest {
val emptyEmail = ""
every { networkConnectionManager.isNetworkConnected } returns true
val viewModel = createViewModel()
val expectedErrorState = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required.asText(
R.string.email_address.asText(),
),
),
emailInput = emptyEmail,
)
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(emptyEmail))
viewModel.trySendAction(MasterPasswordHintAction.SubmitClick)
viewModel.stateFlow.test {
assertEquals(expectedErrorState, awaitItem())
}
}
@Test
fun `SubmitClick with invalid email should show error dialog`() = runTest {
val invalidEmail = "invalidemail"
every { networkConnectionManager.isNetworkConnected } returns true
val viewModel = createViewModel()
val expectedErrorState = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_email.asText(),
),
emailInput = invalidEmail,
)
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(invalidEmail))
viewModel.trySendAction(MasterPasswordHintAction.SubmitClick)
viewModel.stateFlow.test {
assertEquals(expectedErrorState, awaitItem())
}
}
@Test
fun `on DismissDialog should update state to remove dialog`() = runTest {
val initialState = MasterPasswordHintState(
dialog = MasterPasswordHintState.DialogState.Error(message = "Some error".asText()),
emailInput = "test@example.com",
)
val viewModel = createViewModel(initialState)
viewModel.trySendAction(MasterPasswordHintAction.DismissDialog)
viewModel.stateFlow.test {
val expectedState = initialState.copy(dialog = null)
assertEquals(expectedState, awaitItem())
}
}
@Test
fun `on EmailInputChange should update emailInput in state`() = runTest {
val viewModel = createViewModel()
val newEmail = "new@example.com"
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(newEmail))
viewModel.stateFlow.test {
val expectedState = MasterPasswordHintState(
dialog = null,
emailInput = newEmail,
)
assertEquals(expectedState, awaitItem())
}
}
@Test
fun `on CloseClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
@ -31,6 +165,8 @@ class MasterPasswordHintViewModelTest : BaseViewModelTest() {
state: MasterPasswordHintState? = DEFAULT_STATE,
): MasterPasswordHintViewModel = MasterPasswordHintViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
authRepository = authRepository,
networkConnectionManager = networkConnectionManager,
)
}