mirror of
https://github.com/bitwarden/android.git
synced 2025-01-30 11:43:39 +03:00
PM-11604 Network layer for checking email token, nav to UI if needed. (#3862)
This commit is contained in:
parent
ae349183e8
commit
e468ec695b
26 changed files with 756 additions and 56 deletions
|
@ -5,6 +5,7 @@ import android.os.Bundle
|
|||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -76,6 +77,15 @@ class MainActivity : AppCompatActivity() {
|
|||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(
|
||||
baseContext,
|
||||
event.message.invoke(resources),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
|
@ -17,11 +18,15 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
|||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
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 com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
|
@ -56,6 +61,7 @@ class MainViewModel @Inject constructor(
|
|||
settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
|
@ -226,14 +232,7 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
handleCompleteRegistrationData(completeRegistrationData)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
|
@ -310,6 +309,47 @@ class MainViewModel @Inject constructor(
|
|||
sendEvent(MainEvent.Recreate)
|
||||
garbageCollectionManager.tryCollect()
|
||||
}
|
||||
|
||||
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
|
||||
viewModelScope.launch {
|
||||
// Attempt to load the environment for the user if they have a pre-auth environment
|
||||
// saved.
|
||||
environmentRepository.loadEnvironmentForEmail(userEmail = data.email)
|
||||
// Determine if the token is still valid.
|
||||
val emailTokenResult = authRepository.validateEmailToken(
|
||||
email = data.email,
|
||||
token = data.verificationToken,
|
||||
)
|
||||
when (emailTokenResult) {
|
||||
is EmailTokenResult.Error -> {
|
||||
sendEvent(
|
||||
MainEvent.ShowToast(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: R.string.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
EmailTokenResult.Expired -> {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance
|
||||
.RegistrationEvent
|
||||
.ExpiredRegistrationLink
|
||||
}
|
||||
EmailTokenResult.Success -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
completeRegistrationData = data,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -396,4 +436,9 @@ sealed class MainEvent {
|
|||
* Navigate to the debug menu.
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
|
|||
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.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
|
@ -79,4 +80,9 @@ interface UnauthenticatedIdentityApi {
|
|||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): Result<JsonPrimitive?>
|
||||
|
||||
@POST("/accounts/register/verification-email-clicked")
|
||||
suspend fun verifyEmailToken(
|
||||
@Body body: VerifyEmailTokenRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models the request body for verify email token endpoint.
|
||||
*
|
||||
* @param email the email address of the user to verify.
|
||||
* @param token the provided email verification token.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailTokenRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
@SerialName("emailVerificationToken")
|
||||
val token: String,
|
||||
)
|
|
@ -0,0 +1,38 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Model the response of a verify email token request.
|
||||
*
|
||||
* A valid response will be a [VerifyEmailTokenResponseJson.Valid]
|
||||
*
|
||||
* an invalid response will be a [VerifyEmailTokenResponseJson.Invalid] with a message.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerifyEmailTokenResponseJson {
|
||||
|
||||
/**
|
||||
* The token is confirmed as valid from the response.
|
||||
*/
|
||||
@Serializable
|
||||
data object Valid : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The response is invalid.
|
||||
*
|
||||
* @property message The error message. Expected to explain the reason why the token is invalid.
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String,
|
||||
) : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The token has expired. This is special case of similar to [Invalid].
|
||||
*/
|
||||
@Serializable
|
||||
data object TokenExpired : VerifyEmailTokenResponseJson()
|
||||
}
|
|
@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying identity endpoints.
|
||||
|
@ -72,4 +74,12 @@ interface IdentityService {
|
|||
* Register a new account to Bitwarden using email verification flow.
|
||||
*/
|
||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
||||
|
||||
/**
|
||||
* Makes request to verify email registration token. If the token provided is
|
||||
* still valid will return success.
|
||||
*/
|
||||
suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson>
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
|
@ -127,4 +129,35 @@ class IdentityServiceImpl(
|
|||
.sendVerificationEmail(body = body)
|
||||
.map { it?.content }
|
||||
}
|
||||
|
||||
override suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.verifyEmailToken(
|
||||
body = body,
|
||||
)
|
||||
.map {
|
||||
VerifyEmailTokenResponseJson.Valid
|
||||
}
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?.checkForExpiredMessage()
|
||||
?: throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the message body contains text related to the token being expired, return
|
||||
* the TokenExpired type. Otherwise, return the original Invalid response.
|
||||
*/
|
||||
private fun VerifyEmailTokenResponseJson.Invalid.checkForExpiredMessage() =
|
||||
if (message.contains(other = "expired", ignoreCase = true)) {
|
||||
VerifyEmailTokenResponseJson.TokenExpired
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* A manager class for handling authentication fo logging in with remote device.
|
||||
* A manager class for handling authentication for logging in with remote device.
|
||||
*/
|
||||
interface AuthRequestManager {
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
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.EmailTokenResult
|
||||
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.NewSsoUserResult
|
||||
|
@ -377,4 +378,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult
|
||||
|
||||
/**
|
||||
* Validates the given [token] for the given [email]. Part of th new account registration flow.
|
||||
*/
|
||||
suspend fun validateEmailToken(
|
||||
email: String,
|
||||
token: String,
|
||||
): EmailTokenResult
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
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.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
|
@ -41,6 +43,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
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.EmailTokenResult
|
||||
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.NewSsoUserResult
|
||||
|
@ -1256,6 +1259,31 @@ class AuthRepositoryImpl(
|
|||
},
|
||||
)
|
||||
|
||||
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
|
||||
return identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = email,
|
||||
token = token,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (val json = it) {
|
||||
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
|
||||
is VerifyEmailTokenResponseJson.Invalid -> {
|
||||
EmailTokenResult.Error(json.message)
|
||||
}
|
||||
|
||||
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
EmailTokenResult.Error(message = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Model the result of a request to validate a given email token.
|
||||
*/
|
||||
sealed class EmailTokenResult {
|
||||
|
||||
/**
|
||||
* The token is valid and the user can proceed with account creation.
|
||||
*/
|
||||
data object Success : EmailTokenResult()
|
||||
|
||||
/**
|
||||
* The token has expired and is no longer valid.
|
||||
*/
|
||||
data object Expired : EmailTokenResult()
|
||||
|
||||
/**
|
||||
* There was an error validating the token.
|
||||
*/
|
||||
data class Error(val message: String?) : EmailTokenResult()
|
||||
}
|
|
@ -26,7 +26,7 @@ class SpecialCircumstanceManagerImpl(
|
|||
it?.activeAccount?.isLoggedIn == true
|
||||
}
|
||||
.onEach { _ ->
|
||||
if (specialCircumstance is SpecialCircumstance.PreLogin) {
|
||||
if (specialCircumstance is SpecialCircumstance.RegistrationEvent) {
|
||||
specialCircumstance = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
|||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
|
||||
/**
|
||||
* Represents a special circumstance the app may be in. These circumstances could require some kind
|
||||
|
@ -93,11 +92,9 @@ sealed class SpecialCircumstance : Parcelable {
|
|||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
*
|
||||
* @see [SpecialCircumstanceManager.clearSpecialCircumstanceAfterLogin]
|
||||
*/
|
||||
@Parcelize
|
||||
sealed class PreLogin : SpecialCircumstance() {
|
||||
sealed class RegistrationEvent : SpecialCircumstance() {
|
||||
/**
|
||||
* The app was launched via AppLink in order to allow the user complete an ongoing
|
||||
* registration.
|
||||
|
@ -106,6 +103,13 @@ sealed class SpecialCircumstance : Parcelable {
|
|||
data class CompleteRegistration(
|
||||
val completeRegistrationData: CompleteRegistrationData,
|
||||
val timestamp: Long,
|
||||
) : PreLogin()
|
||||
) : RegistrationEvent()
|
||||
|
||||
/**
|
||||
* The app was launched via AppLink in order to allow the user to complete registration but,
|
||||
* the registration link has expired.
|
||||
*/
|
||||
@Parcelize
|
||||
data object ExpiredRegistrationLink : RegistrationEvent()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,8 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
|||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
is SpecialCircumstance.PreLogin.CompleteRegistration -> null
|
||||
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,7 +39,8 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
|
|||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
is SpecialCircumstance.PreLogin.CompleteRegistration -> null
|
||||
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -52,6 +53,13 @@ fun ExpiredRegistrationLinkScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
val sendCloseClicked = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(onBack = sendCloseClicked)
|
||||
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
|
@ -61,11 +69,7 @@ fun ExpiredRegistrationLinkScreen(
|
|||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||
}
|
||||
},
|
||||
onNavigationIconClick = sendCloseClicked,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* View model for the [ExpiredRegistrationLinkScreen].
|
||||
*/
|
||||
class ExpiredRegistrationLinkViewModel @Inject constructor() :
|
||||
BaseViewModel<Unit, ExpiredRegistrationLinkEvent, ExpiredRegistrationLinkAction>(
|
||||
@HiltViewModel
|
||||
class ExpiredRegistrationLinkViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
) : BaseViewModel<Unit, ExpiredRegistrationLinkEvent, ExpiredRegistrationLinkAction>(
|
||||
initialState = Unit,
|
||||
) {
|
||||
override fun handleAction(action: ExpiredRegistrationLinkAction) {
|
||||
|
@ -21,16 +25,28 @@ class ExpiredRegistrationLinkViewModel @Inject constructor() :
|
|||
}
|
||||
|
||||
private fun handleRestartRegistrationClicked() {
|
||||
resetPendingAccountAddition()
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToStartRegistration)
|
||||
}
|
||||
|
||||
private fun handleGoToLoginClicked() {
|
||||
resetPendingAccountAddition()
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToLogin)
|
||||
}
|
||||
|
||||
private fun handleCloseClicked() {
|
||||
resetPendingAccountAddition()
|
||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* Since leaving the expired registration screen takes the user back to the landing
|
||||
* screen we want to update the [AuthRepository] to add a pending account addition.
|
||||
* This will help ensure we are in the correct user state when returning to the landing screen.
|
||||
*/
|
||||
private fun resetPendingAccountAddition() {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -70,13 +70,8 @@ class RootNavViewModel @Inject constructor(
|
|||
|
||||
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
|
||||
|
||||
specialCircumstance is SpecialCircumstance.PreLogin.CompleteRegistration -> {
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = specialCircumstance.completeRegistrationData.email,
|
||||
verificationToken = specialCircumstance.completeRegistrationData.verificationToken,
|
||||
fromEmail = specialCircumstance.completeRegistrationData.fromEmail,
|
||||
timestamp = specialCircumstance.timestamp,
|
||||
)
|
||||
specialCircumstance is SpecialCircumstance.RegistrationEvent -> {
|
||||
getRegistrationEventNavState(specialCircumstance)
|
||||
}
|
||||
|
||||
userState == null ||
|
||||
|
@ -141,7 +136,7 @@ class RootNavViewModel @Inject constructor(
|
|||
null,
|
||||
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
|
||||
|
||||
is SpecialCircumstance.PreLogin.CompleteRegistration -> {
|
||||
is SpecialCircumstance.RegistrationEvent -> {
|
||||
throw IllegalStateException(
|
||||
"Special circumstance should have been already handled.",
|
||||
)
|
||||
|
@ -154,6 +149,23 @@ class RootNavViewModel @Inject constructor(
|
|||
mutableStateFlow.update { updatedRootNavState }
|
||||
}
|
||||
|
||||
private fun getRegistrationEventNavState(
|
||||
registrationEvent: SpecialCircumstance.RegistrationEvent,
|
||||
): RootNavState = when (registrationEvent) {
|
||||
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> {
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = registrationEvent.completeRegistrationData.email,
|
||||
verificationToken = registrationEvent.completeRegistrationData.verificationToken,
|
||||
fromEmail = registrationEvent.completeRegistrationData.fromEmail,
|
||||
timestamp = registrationEvent.timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> {
|
||||
RootNavState.ExpiredRegistrationLink
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserState.shouldShowRemovePassword(authState: AuthState): Boolean {
|
||||
val isLoggedInUsingSso = (authState as? AuthState.Authenticated)
|
||||
?.accessToken
|
||||
|
|
|
@ -994,4 +994,5 @@ Do you want to switch to this account?</string>
|
|||
<string name="restart_registration">Restart registration</string>
|
||||
<string name="authenticator_sync">Authenticator Sync</string>
|
||||
<string name="allow_bitwarden_authenticator_syncing">Allow Bitwarden Authenticator Syncing</string>
|
||||
<string name="there_was_an_issue_validating_the_registration_token">There was an issue validating the registration token.</string>
|
||||
</resources>
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
|
@ -34,12 +35,14 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
|
|||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
|
@ -81,6 +84,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { switchAccount(any()) } returns SwitchAccountResult.NoChange
|
||||
coEvery { validateEmailToken(any(), any()) } returns EmailTokenResult.Success
|
||||
}
|
||||
private val mutableVaultStateEventFlow = bufferedMutableSharedFlow<VaultStateEvent>()
|
||||
private val vaultRepository = mockk<VaultRepository> {
|
||||
|
@ -95,6 +99,9 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
authRepository = mockAuthRepository,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
private val environmentRepository = mockk<EnvironmentRepository>(relaxed = true) {
|
||||
every { loadEnvironmentForEmail(any()) } returns true
|
||||
}
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { getShareDataFromIntent(any()) } returns null
|
||||
}
|
||||
|
@ -326,9 +333,12 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to CompleteRegistration`() {
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to CompleteRegistration if token is valid`() {
|
||||
val viewModel = createViewModel()
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData>()
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns "email"
|
||||
every { verificationToken } returns "token"
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
|
@ -346,14 +356,173 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = FIXED_CLOCK.millis(),
|
||||
),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
|
||||
verify(exactly = 0) { authRepository.hasPendingAccountAddition = true }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should set pending account addition to true if there is an active user`() {
|
||||
val viewModel = createViewModel()
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns "email"
|
||||
every { verificationToken } returns "token"
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { authRepository.activeUserId } returns "activeId"
|
||||
every { authRepository.hasPendingAccountAddition = true } just runs
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = FIXED_CLOCK.millis(),
|
||||
),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
verify { authRepository.hasPendingAccountAddition = true }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to ExpiredRegistration if token is not valid`() {
|
||||
val viewModel = createViewModel()
|
||||
val intentEmail = "email"
|
||||
val token = "token"
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns intentEmail
|
||||
every { verificationToken } returns token
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { authRepository.activeUserId } returns null
|
||||
coEvery {
|
||||
authRepository.validateEmailToken(
|
||||
email = intentEmail,
|
||||
token = token,
|
||||
)
|
||||
} returns EmailTokenResult.Expired
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink,
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should show toast if token is not valid but unable to determine reason`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val intentEmail = "email"
|
||||
val token = "token"
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns intentEmail
|
||||
every { verificationToken } returns token
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { authRepository.activeUserId } returns null
|
||||
coEvery {
|
||||
authRepository.validateEmailToken(
|
||||
intentEmail,
|
||||
token,
|
||||
)
|
||||
} returns EmailTokenResult.Error(message = null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
MainEvent.ShowToast(R.string.there_was_an_issue_validating_the_registration_token.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should show toast with custom message if token is not valid but unable to determine reason`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val intentEmail = "email"
|
||||
val token = "token"
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns intentEmail
|
||||
every { verificationToken } returns token
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { authRepository.activeUserId } returns null
|
||||
|
||||
val expectedMessage = "expectedMessage"
|
||||
coEvery {
|
||||
authRepository.validateEmailToken(
|
||||
intentEmail,
|
||||
token,
|
||||
)
|
||||
} returns EmailTokenResult.Error(message = expectedMessage)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
MainEvent.ShowToast(expectedMessage.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() {
|
||||
|
@ -804,6 +973,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository = vaultRepository,
|
||||
authRepository = authRepository,
|
||||
clock = FIXED_CLOCK,
|
||||
environmentRepository = environmentRepository,
|
||||
savedStateHandle = savedStateHandle.apply {
|
||||
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)
|
||||
},
|
||||
|
|
|
@ -16,6 +16,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEm
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
|
@ -369,6 +371,77 @@ class IdentityServiceTest : BaseServiceTest() {
|
|||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken should return Valid when response is success`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(200))
|
||||
val result = identityService.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
token = EMAIL_TOKEN,
|
||||
email = EMAIL,
|
||||
),
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `verifyEmailToken should return TokenExpired when response is expired error`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"message": "Expired link. Please restart registration or try logging in. You may already have an account"
|
||||
}
|
||||
""".trimIndent()
|
||||
val response = MockResponse().setResponseCode(400).setBody(json)
|
||||
server.enqueue(response)
|
||||
val result = identityService.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
token = EMAIL_TOKEN,
|
||||
email = EMAIL,
|
||||
),
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResponseJson.TokenExpired,
|
||||
result.getOrThrow(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `verifyEmailToken should return Invalid when response message is non expired error`() = runTest {
|
||||
val messageWithOutExpired = "message without expir... whoops"
|
||||
val json = """
|
||||
{
|
||||
"message": "$messageWithOutExpired"
|
||||
}
|
||||
""".trimIndent()
|
||||
val response = MockResponse().setResponseCode(400).setBody(json)
|
||||
server.enqueue(response)
|
||||
val result = identityService.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
token = EMAIL_TOKEN,
|
||||
email = EMAIL,
|
||||
),
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResponseJson.Invalid(messageWithOutExpired),
|
||||
result.getOrThrow(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken should return an error when response is an un-handled error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
val result = identityService.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
token = EMAIL_TOKEN,
|
||||
),
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||
private const val REFRESH_TOKEN = "refreshToken"
|
||||
|
|
|
@ -42,6 +42,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserD
|
|||
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.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
|
@ -61,6 +63,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
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.EmailTokenResult
|
||||
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.NewSsoUserResult
|
||||
|
@ -6012,6 +6015,90 @@ class AuthRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateEmailToken should return success result when service returns success`() = runTest {
|
||||
coEvery {
|
||||
identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
token = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns VerifyEmailTokenResponseJson.Valid.asSuccess()
|
||||
|
||||
val emailTokenResult = repository.validateEmailToken(EMAIL, EMAIL_VERIFICATION_TOKEN)
|
||||
|
||||
assertEquals(
|
||||
EmailTokenResult.Success,
|
||||
emailTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateEmailToken should return expired result when service returns TokenExpired`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
token = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns VerifyEmailTokenResponseJson.TokenExpired.asSuccess()
|
||||
|
||||
val emailTokenResult = repository.validateEmailToken(EMAIL, EMAIL_VERIFICATION_TOKEN)
|
||||
|
||||
assertEquals(
|
||||
EmailTokenResult.Expired,
|
||||
emailTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateEmailToken should return error result when service returns error without expired message`() =
|
||||
runTest {
|
||||
val errorMessage = "I haven't heard of second breakfast."
|
||||
coEvery {
|
||||
identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
token = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns VerifyEmailTokenResponseJson.Invalid(message = errorMessage).asSuccess()
|
||||
|
||||
val emailTokenResult = repository.validateEmailToken(EMAIL, EMAIL_VERIFICATION_TOKEN)
|
||||
|
||||
assertEquals(
|
||||
EmailTokenResult.Error(message = errorMessage),
|
||||
emailTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateEmailToken should return error result when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
token = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns Exception().asFailure()
|
||||
|
||||
val emailTokenResult = repository.validateEmailToken(EMAIL, EMAIL_VERIFICATION_TOKEN)
|
||||
|
||||
assertEquals(
|
||||
EmailTokenResult.Error(message = null),
|
||||
emailTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||
private const val NAME = "Example Name"
|
||||
|
|
|
@ -51,7 +51,7 @@ class SpecialCircumstanceManagerTest {
|
|||
assertNull(awaitItem())
|
||||
|
||||
val preLoginSpecialCircumstance =
|
||||
mockk<SpecialCircumstance.PreLogin.CompleteRegistration>()
|
||||
mockk<SpecialCircumstance.RegistrationEvent.CompleteRegistration>()
|
||||
|
||||
specialCircumstanceManager.specialCircumstance = preLoginSpecialCircumstance
|
||||
assertEquals(preLoginSpecialCircumstance, awaitItem())
|
||||
|
|
|
@ -20,7 +20,7 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.PreLogin
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.RegistrationEvent
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
|
@ -93,7 +93,8 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
||||
}
|
||||
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
||||
private val mockCompleteRegistrationCircumstance = mockk<PreLogin.CompleteRegistration>()
|
||||
private val mockCompleteRegistrationCircumstance =
|
||||
mockk<RegistrationEvent.CompleteRegistration>()
|
||||
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
|
||||
every { generatorResultFlow } returns mutableGeneratorResultFlow
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class ExpiredRegistrationLinkScreenTest : BaseComposeTest() {
|
|||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
setContentWithBackDispatcher {
|
||||
ExpiredRegistrationLinkScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToLogin = { onNavigateToLoginCalled = true },
|
||||
|
@ -35,6 +35,12 @@ class ExpiredRegistrationLinkScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `System back event invokes CloseClicked action`() {
|
||||
backDispatcher?.onBackPressed()
|
||||
verify { viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClicked sends NavigateBack action`() {
|
||||
composeTestRule
|
||||
|
|
|
@ -1,37 +1,51 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ExpiredRegistrationLinkViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val authRepository = mockk<AuthRepository>(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `CloseClicked sends NavigateBack event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
fun `CloseClicked sends NavigateBack event and resets pending account addition`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
verify { authRepository.hasPendingAccountAddition = true }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `RestartRegistrationClicked sends NavigateToStartRegistration event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToStartRegistration, awaitItem())
|
||||
fun `RestartRegistrationClicked sends NavigateToStartRegistration event and resets pending account addition`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToStartRegistration, awaitItem())
|
||||
}
|
||||
verify { authRepository.hasPendingAccountAddition = true }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GoToLoginClicked sends NavigateToLogin event`() = runTest {
|
||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToLogin, awaitItem())
|
||||
fun `GoToLoginClicked sends NavigateToLogin event and resets pending account addition`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked)
|
||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToLogin, awaitItem())
|
||||
}
|
||||
verify { authRepository.hasPendingAccountAddition = true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): ExpiredRegistrationLinkViewModel =
|
||||
ExpiredRegistrationLinkViewModel(authRepository = authRepository)
|
||||
}
|
||||
|
|
|
@ -663,7 +663,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
every { authRepository.hasPendingAccountAddition } returns false
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
|
@ -690,7 +690,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
|
@ -740,7 +740,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
|
@ -784,6 +784,97 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when there are no accounts but there is a ExpiredRegistrationLink special circumstance the nav state should be ExpiredRegistrationLink`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns false
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink
|
||||
mutableUserStateFlow.tryEmit(null)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.ExpiredRegistrationLink,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is a ExpiredRegistrationLink special circumstance the nav state should be ExpiredRegistrationLink`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarHexColor",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.ExpiredRegistrationLink,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has a locked vault but there is a ExpiredRegistrationLink special circumstance the nav state should be ExpiredRegistrationLink`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = false,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.ExpiredRegistrationLink,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
|
Loading…
Add table
Reference in a new issue