From 5fa49c8b53800b80850da03787e62b21e870ac4b Mon Sep 17 00:00:00 2001 From: Shannon Draeker <125921730+shannon-livefront@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:48:08 -0700 Subject: [PATCH] BIT-918: Resend notification emails (#792) --- .../datasource/network/api/AccountsApi.kt | 7 +- .../network/model/ResendEmailJsonRequest.kt | 29 ++++++++ .../network/service/AccountsService.kt | 6 ++ .../network/service/AccountsServiceImpl.kt | 4 ++ .../data/auth/repository/AuthRepository.kt | 6 ++ .../auth/repository/AuthRepositoryImpl.kt | 29 ++++++++ .../repository/model/ResendEmailResult.kt | 17 +++++ .../twofactorlogin/TwoFactorLoginScreen.kt | 2 +- .../twofactorlogin/TwoFactorLoginViewModel.kt | 72 ++++++++++++++++++- .../network/service/AccountsServiceTest.kt | 16 +++++ .../auth/repository/AuthRepositoryTest.kt | 68 ++++++++++++++++++ .../TwoFactorLoginViewModelTest.kt | 62 ++++++++++++++-- 12 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/ResendEmailJsonRequest.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/ResendEmailResult.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt index df58c94f3..f41e7a20c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest import retrofit2.http.Body import retrofit2.http.POST @@ -12,7 +13,6 @@ import retrofit2.http.POST * Defines raw calls under the /accounts API. */ interface AccountsApi { - @POST("/accounts/prelogin") suspend fun preLogin(@Body body: PreLoginRequestJson): Result @@ -23,4 +23,9 @@ interface AccountsApi { suspend fun passwordHintRequest( @Body body: PasswordHintRequestJson, ): Result + + @POST("/two-factor/send-email-login") + suspend fun resendVerificationCodeEmail( + @Body body: ResendEmailJsonRequest, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/ResendEmailJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/ResendEmailJsonRequest.kt new file mode 100644 index 000000000..3dcc0bb79 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/ResendEmailJsonRequest.kt @@ -0,0 +1,29 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Hold the information necessary to resend the email with the + * two-factor verification code. + * + * @property deviceIdentifier The device identifier. + * @property email The user's email address. + * @property passwordHash The master password hash, if the user is logging + * in via the master password. + * @property ssoToken The sso token, if the user is logging in via single sign on. + */ +@Serializable +data class ResendEmailJsonRequest( + @SerialName("DeviceIdentifier") + val deviceIdentifier: String, + + @SerialName("Email") + val email: String, + + @SerialName("MasterPasswordHash") + val passwordHash: String?, + + @SerialName("SsoEmail2FaSessionToken") + val ssoToken: String?, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsService.kt index 4d850c4a7..77097820d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsService.kt @@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest /** * Provides an API for querying accounts endpoints. @@ -29,4 +30,9 @@ interface AccountsService { * Request a password hint. */ suspend fun requestPasswordHint(email: String): Result + + /** + * Resend the email with the two-factor verification code. + */ + suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt index 85dbfaf5e..9bf1d9b35 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull import kotlinx.serialization.json.Json @@ -58,4 +59,7 @@ class AccountsServiceImpl constructor( ) ?: throw throwable } + + override suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result = + accountsApi.resendVerificationCodeEmail(body = body) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index bea12001d..6d7432a4e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -13,6 +13,7 @@ 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 +import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -106,6 +107,11 @@ interface AuthRepository : AuthenticatorProvider { */ fun logout() + /** + * Resend the email with the two-factor verification code. + */ + suspend fun resendVerificationCodeEmail(): ResendEmailResult + /** * Switches to the account corresponding to the given [userId] if possible. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 58519570f..c7fe115b9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService @@ -34,6 +35,7 @@ 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 +import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -96,6 +98,11 @@ class AuthRepositoryImpl( */ private var identityTokenAuthModel: IdentityTokenAuthModel? = null + /** + * The information necessary to resend the verification code email for two-factor login. + */ + private var resendEmailJsonRequest: ResendEmailJsonRequest? = null + /** * A scope intended for use when simply collecting multiple flows in order to combine them. The * use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of @@ -272,9 +279,22 @@ class AuthRepositoryImpl( onSuccess = { loginResponse -> when (loginResponse) { is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey) + is TwoFactorRequired -> { + // Cache the data necessary for the remaining two-factor auth flow. identityTokenAuthModel = authModel twoFactorResponse = loginResponse + resendEmailJsonRequest = ResendEmailJsonRequest( + deviceIdentifier = authDiskSource.uniqueAppId, + email = email, + passwordHash = authModel.password, + ssoToken = loginResponse.ssoToken, + ) + + // If this error was received, it also means any cached two-factor + // token is invalid. + authDiskSource.storeTwoFactorToken(email, null) + LoginResult.TwoFactorRequired } @@ -300,6 +320,7 @@ class AuthRepositoryImpl( // Remove any cached data after successfully logging in. identityTokenAuthModel = null twoFactorResponse = null + resendEmailJsonRequest = null // Attempt to unlock the vault if possible. password?.let { @@ -373,6 +394,14 @@ class AuthRepositoryImpl( if (wasActiveUser) vaultRepository.clearUnlockedData() } + override suspend fun resendVerificationCodeEmail(): ResendEmailResult = + resendEmailJsonRequest?.let { jsonRequest -> + accountsService.resendVerificationCodeEmail(body = jsonRequest).fold( + onFailure = { ResendEmailResult.Error(message = it.message) }, + onSuccess = { ResendEmailResult.Success }, + ) + } ?: ResendEmailResult.Error(message = null) + @Suppress("ReturnCount") override fun switchAccount(userId: String): SwitchAccountResult { val currentUserState = authDiskSource.userState diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/ResendEmailResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/ResendEmailResult.kt new file mode 100644 index 000000000..5375b1d5e --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/ResendEmailResult.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +/** + * Models result of resend email request. + */ +sealed class ResendEmailResult { + + /** + * Resend email request success + */ + data object Success : ResendEmailResult() + + /** + * There was an error. + */ + data class Error(val message: String?) : ResendEmailResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt index 1c8faefa1..ba5db5659 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt @@ -81,7 +81,7 @@ fun TwoFactorLoginScreen( } is TwoFactorLoginEvent.ShowToast -> { - Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index 042b2589e..6c9923e2a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.util.preferredAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.util.twoFactorDisplayEmail import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.LoginResult +import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -31,6 +32,7 @@ private const val KEY_STATE = "state" * Manages application state for the Two-Factor Login screen. */ @HiltViewModel +@Suppress("TooManyFunctions") class TwoFactorLoginViewModel @Inject constructor( private val authRepository: AuthRepository, savedStateHandle: SavedStateHandle, @@ -83,6 +85,9 @@ class TwoFactorLoginViewModel @Inject constructor( } is TwoFactorLoginAction.Internal.ReceiveLoginResult -> handleReceiveLoginResult(action) + is TwoFactorLoginAction.Internal.ReceiveResendEmailResult -> { + handleReceiveResendEmailResult(action) + } } } @@ -213,6 +218,39 @@ class TwoFactorLoginViewModel @Inject constructor( } } + /** + * Handle the resend email result. + */ + private fun handleReceiveResendEmailResult( + action: TwoFactorLoginAction.Internal.ReceiveResendEmailResult, + ) { + // Dismiss the loading overlay. + mutableStateFlow.update { it.copy(dialogState = null) } + + when (action.resendEmailResult) { + // Display a dialog for an error result. + is ResendEmailResult.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = TwoFactorLoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.verification_email_not_sent.asText(), + ), + ) + } + } + + // Display a toast for a successful result. + ResendEmailResult.Success -> { + sendEvent( + TwoFactorLoginEvent.ShowToast( + message = R.string.verification_email_sent.asText(), + ), + ) + } + } + } + /** * Update the state with the new toggle value. */ @@ -228,8 +266,29 @@ class TwoFactorLoginViewModel @Inject constructor( * Resend the verification code email. */ private fun handleResendEmailClick() { - // TODO: Finish implementation (BIT-918) - sendEvent(TwoFactorLoginEvent.ShowToast("Not yet implemented")) + // Ensure that the user is in fact verifying with email. + if (mutableStateFlow.value.authMethod != TwoFactorAuthMethod.EMAIL) { + return + } + + // Show the loading overlay. + mutableStateFlow.update { + it.copy( + dialogState = TwoFactorLoginState.DialogState.Loading( + message = R.string.submitting.asText(), + ), + ) + } + + // Resend the email notification. + viewModelScope.launch { + val result = authRepository.resendVerificationCodeEmail() + sendAction( + TwoFactorLoginAction.Internal.ReceiveResendEmailResult( + resendEmailResult = result, + ), + ) + } } /** @@ -312,7 +371,7 @@ sealed class TwoFactorLoginEvent { * Shows a toast with the given [message]. */ data class ShowToast( - val message: String, + val message: Text, ) : TwoFactorLoginEvent() } @@ -378,5 +437,12 @@ sealed class TwoFactorLoginAction { data class ReceiveLoginResult( val loginResult: LoginResult, ) : Internal() + + /** + * Indicates a resend email result has been received. + */ + data class ReceiveResendEmailResult( + val resendEmailResult: ResendEmailResult, + ) : Internal() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt index 330c79248..19691da86 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest import com.x8bit.bitwarden.data.platform.base.BaseServiceTest import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json @@ -211,6 +212,21 @@ class AccountsServiceTest : BaseServiceTest() { assertEquals(PasswordHintResponseJson.Success, result.getOrNull()) } + @Test + fun `resendVerificationCodeEmail with empty response is success`() = runTest { + val response = MockResponse().setBody("") + server.enqueue(response) + val result = service.resendVerificationCodeEmail( + body = ResendEmailJsonRequest( + deviceIdentifier = "3", + email = "example@email.com", + passwordHash = "37y4d8r379r4789nt387r39k3dr87nr93", + ssoToken = null, + ), + ) + assertTrue(result.isSuccess) + } + companion object { private const val EMAIL = "email" private val registerRequestBody = RegisterRequestJson( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index d997f58eb..165fa708e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResp 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 +import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService @@ -45,6 +46,7 @@ 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 +import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations @@ -1418,6 +1420,72 @@ class AuthRepositoryTest { verify(exactly = 0) { vaultRepository.clearUnlockedData() } } + @Test + fun `resendVerificationCodeEmail uses cached request data to make api call`() = runTest { + // Attempt a normal login with a two factor error first, so that the necessary + // data will be cached. + coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS) + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns Result.success( + GetTokenResponseJson.TwoFactorRequired( + TWO_FACTOR_AUTH_METHODS_DATA, null, null, + ), + ) + val firstResult = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.TwoFactorRequired, firstResult) + coVerify { accountsService.preLogin(email = EMAIL) } + coVerify { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } + + // Resend the verification code email. + coEvery { + accountsService.resendVerificationCodeEmail( + body = ResendEmailJsonRequest( + deviceIdentifier = UNIQUE_APP_ID, + email = EMAIL, + passwordHash = PASSWORD_HASH, + ssoToken = null, + ), + ) + } returns Result.success(Unit) + val resendEmailResult = repository.resendVerificationCodeEmail() + assertEquals(ResendEmailResult.Success, resendEmailResult) + coVerify { + accountsService.resendVerificationCodeEmail( + body = ResendEmailJsonRequest( + deviceIdentifier = UNIQUE_APP_ID, + email = EMAIL, + passwordHash = PASSWORD_HASH, + ssoToken = null, + ), + ) + } + } + + @Test + fun `resendVerificationCodeEmail returns error if no request data cached`() = runTest { + val result = repository.resendVerificationCodeEmail() + assertEquals(ResendEmailResult.Error(message = null), result) + } + @Test fun `switchAccount when there is no saved UserState should do nothing`() { val updatedUserId = USER_ID_2 diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt index cb5a5131a..55498c943 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMetho import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.LoginResult +import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow @@ -239,7 +240,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { } @Test - fun `ContinueButtonClick login returns Error should update errorStateDialog`() = runTest { + fun `ContinueButtonClick login returns Error should update dialogState`() = runTest { coEvery { authRepository.login( email = "example@email.com", @@ -309,17 +310,70 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { } @Test - fun `ResendEmailClick should emit ShowToast`() = runTest { + fun `ResendEmailClick returns success should emit ShowToast`() = runTest { + coEvery { + authRepository.resendVerificationCodeEmail() + } returns ResendEmailResult.Success + val viewModel = createViewModel() viewModel.eventFlow.test { - viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) + viewModel.actionChannel.trySend( + TwoFactorLoginAction.SelectAuthMethod( + TwoFactorAuthMethod.EMAIL, + ), + ) assertEquals( - TwoFactorLoginEvent.ShowToast("Not yet implemented"), + DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.EMAIL), + viewModel.stateFlow.value, + ) + + viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) + + assertEquals( + DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.EMAIL), + viewModel.stateFlow.value, + ) + + assertEquals( + TwoFactorLoginEvent.ShowToast(message = R.string.verification_email_sent.asText()), awaitItem(), ) } } + @Test + fun `ResendEmailClick returns error should update dialogState`() = runTest { + coEvery { + authRepository.resendVerificationCodeEmail() + } returns ResendEmailResult.Error(message = null) + + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + TwoFactorLoginAction.SelectAuthMethod( + TwoFactorAuthMethod.EMAIL, + ), + ) + assertEquals( + DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.EMAIL), + viewModel.stateFlow.value, + ) + + viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) + + assertEquals( + DEFAULT_STATE.copy( + authMethod = TwoFactorAuthMethod.EMAIL, + dialogState = TwoFactorLoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.verification_email_not_sent.asText(), + ), + ), + viewModel.stateFlow.value, + ) + } + } + @Test fun `SelectAuthMethod with RECOVERY_CODE should launch the NavigateToRecoveryCode event`() = runTest {