BIT-918: Resend notification emails (#792)

This commit is contained in:
Shannon Draeker 2024-01-25 17:48:08 -07:00 committed by Álison Fernandes
parent 555ff1dcd2
commit 5fa49c8b53
12 changed files with 309 additions and 9 deletions

View file

@ -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.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST import retrofit2.http.POST
@ -12,7 +13,6 @@ import retrofit2.http.POST
* Defines raw calls under the /accounts API. * Defines raw calls under the /accounts API.
*/ */
interface AccountsApi { interface AccountsApi {
@POST("/accounts/prelogin") @POST("/accounts/prelogin")
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson> suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
@ -23,4 +23,9 @@ interface AccountsApi {
suspend fun passwordHintRequest( suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson, @Body body: PasswordHintRequestJson,
): Result<Unit> ): Result<Unit>
@POST("/two-factor/send-email-login")
suspend fun resendVerificationCodeEmail(
@Body body: ResendEmailJsonRequest,
): Result<Unit>
} }

View file

@ -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?,
)

View file

@ -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.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
/** /**
* Provides an API for querying accounts endpoints. * Provides an API for querying accounts endpoints.
@ -29,4 +30,9 @@ interface AccountsService {
* Request a password hint. * Request a password hint.
*/ */
suspend fun requestPasswordHint(email: String): Result<PasswordHintResponseJson> suspend fun requestPasswordHint(email: String): Result<PasswordHintResponseJson>
/**
* Resend the email with the two-factor verification code.
*/
suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result<Unit>
} }

View file

@ -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.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.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.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -58,4 +59,7 @@ class AccountsServiceImpl constructor(
) )
?: throw throwable ?: throw throwable
} }
override suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result<Unit> =
accountsApi.resendVerificationCodeEmail(body = body)
} }

View file

@ -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.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult 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.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.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.UserState
@ -106,6 +107,11 @@ interface AuthRepository : AuthenticatorProvider {
*/ */
fun logout() 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. * Switches to the account corresponding to the given [userId] if possible.
*/ */

View file

@ -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.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.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.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService 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.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult 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.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.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.UserState
@ -96,6 +98,11 @@ class AuthRepositoryImpl(
*/ */
private var identityTokenAuthModel: IdentityTokenAuthModel? = null 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 * 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 * use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
@ -272,9 +279,22 @@ class AuthRepositoryImpl(
onSuccess = { loginResponse -> onSuccess = { loginResponse ->
when (loginResponse) { when (loginResponse) {
is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey) is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey)
is TwoFactorRequired -> { is TwoFactorRequired -> {
// Cache the data necessary for the remaining two-factor auth flow.
identityTokenAuthModel = authModel identityTokenAuthModel = authModel
twoFactorResponse = loginResponse 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 LoginResult.TwoFactorRequired
} }
@ -300,6 +320,7 @@ class AuthRepositoryImpl(
// Remove any cached data after successfully logging in. // Remove any cached data after successfully logging in.
identityTokenAuthModel = null identityTokenAuthModel = null
twoFactorResponse = null twoFactorResponse = null
resendEmailJsonRequest = null
// Attempt to unlock the vault if possible. // Attempt to unlock the vault if possible.
password?.let { password?.let {
@ -373,6 +394,14 @@ class AuthRepositoryImpl(
if (wasActiveUser) vaultRepository.clearUnlockedData() 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") @Suppress("ReturnCount")
override fun switchAccount(userId: String): SwitchAccountResult { override fun switchAccount(userId: String): SwitchAccountResult {
val currentUserState = authDiskSource.userState val currentUserState = authDiskSource.userState

View file

@ -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()
}

View file

@ -81,7 +81,7 @@ fun TwoFactorLoginScreen(
} }
is TwoFactorLoginEvent.ShowToast -> { is TwoFactorLoginEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
} }
} }
} }

View file

@ -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.datasource.network.util.twoFactorDisplayEmail
import com.x8bit.bitwarden.data.auth.repository.AuthRepository 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.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.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel 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. * Manages application state for the Two-Factor Login screen.
*/ */
@HiltViewModel @HiltViewModel
@Suppress("TooManyFunctions")
class TwoFactorLoginViewModel @Inject constructor( class TwoFactorLoginViewModel @Inject constructor(
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
@ -83,6 +85,9 @@ class TwoFactorLoginViewModel @Inject constructor(
} }
is TwoFactorLoginAction.Internal.ReceiveLoginResult -> handleReceiveLoginResult(action) 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. * Update the state with the new toggle value.
*/ */
@ -228,8 +266,29 @@ class TwoFactorLoginViewModel @Inject constructor(
* Resend the verification code email. * Resend the verification code email.
*/ */
private fun handleResendEmailClick() { private fun handleResendEmailClick() {
// TODO: Finish implementation (BIT-918) // Ensure that the user is in fact verifying with email.
sendEvent(TwoFactorLoginEvent.ShowToast("Not yet implemented")) 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]. * Shows a toast with the given [message].
*/ */
data class ShowToast( data class ShowToast(
val message: String, val message: Text,
) : TwoFactorLoginEvent() ) : TwoFactorLoginEvent()
} }
@ -378,5 +437,12 @@ sealed class TwoFactorLoginAction {
data class ReceiveLoginResult( data class ReceiveLoginResult(
val loginResult: LoginResult, val loginResult: LoginResult,
) : Internal() ) : Internal()
/**
* Indicates a resend email result has been received.
*/
data class ReceiveResendEmailResult(
val resendEmailResult: ResendEmailResult,
) : Internal()
} }
} }

View file

@ -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.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -211,6 +212,21 @@ class AccountsServiceTest : BaseServiceTest() {
assertEquals(PasswordHintResponseJson.Success, result.getOrNull()) 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 { companion object {
private const val EMAIL = "email" private const val EMAIL = "email"
private val registerRequestBody = RegisterRequestJson( private val registerRequestBody = RegisterRequestJson(

View file

@ -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.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson 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.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.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService 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.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult 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.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.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
@ -1418,6 +1420,72 @@ class AuthRepositoryTest {
verify(exactly = 0) { vaultRepository.clearUnlockedData() } 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 @Test
fun `switchAccount when there is no saved UserState should do nothing`() { fun `switchAccount when there is no saved UserState should do nothing`() {
val updatedUserId = USER_ID_2 val updatedUserId = USER_ID_2

View file

@ -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.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.repository.AuthRepository 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.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.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@ -239,7 +240,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ContinueButtonClick login returns Error should update errorStateDialog`() = runTest { fun `ContinueButtonClick login returns Error should update dialogState`() = runTest {
coEvery { coEvery {
authRepository.login( authRepository.login(
email = "example@email.com", email = "example@email.com",
@ -309,17 +310,70 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `ResendEmailClick should emit ShowToast`() = runTest { fun `ResendEmailClick returns success should emit ShowToast`() = runTest {
coEvery {
authRepository.resendVerificationCodeEmail()
} returns ResendEmailResult.Success
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) viewModel.actionChannel.trySend(
TwoFactorLoginAction.SelectAuthMethod(
TwoFactorAuthMethod.EMAIL,
),
)
assertEquals( 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(), 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 @Test
fun `SelectAuthMethod with RECOVERY_CODE should launch the NavigateToRecoveryCode event`() = fun `SelectAuthMethod with RECOVERY_CODE should launch the NavigateToRecoveryCode event`() =
runTest { runTest {