mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
BIT-918: Resend notification emails (#792)
This commit is contained in:
parent
555ff1dcd2
commit
5fa49c8b53
12 changed files with 309 additions and 9 deletions
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?,
|
||||||
|
)
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue