mirror of
https://github.com/bitwarden/android.git
synced 2025-02-19 21:39:57 +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
app/src
main
java/com/x8bit/bitwarden
MainActivity.ktMainViewModel.kt
data
auth
datasource/network
api
model
service
manager
repository
platform/manager
ui
auth/feature/expiredregistrationlink
platform/feature/rootnav
res/values
test/java/com/x8bit/bitwarden
MainViewModelTest.kt
data
auth
platform/manager
ui
auth/feature
completeregistration
expiredregistrationlink
platform/feature/rootnav
|
@ -5,6 +5,7 @@ import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -76,6 +77,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||||
MainEvent.Recreate -> handleRecreate()
|
MainEvent.Recreate -> handleRecreate()
|
||||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||||
|
is MainEvent.ShowToast -> {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
baseContext,
|
||||||
|
event.message.invoke(resources),
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitwarden.vault.CipherView
|
import com.bitwarden.vault.CipherView
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
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.getCompleteRegistrationDataIntentOrNull
|
||||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
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.autofill.util.getAutofillSelectionDataOrNull
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
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.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.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
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.feature.settings.appearance.model.AppTheme
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||||
|
@ -56,6 +61,7 @@ class MainViewModel @Inject constructor(
|
||||||
settingsRepository: SettingsRepository,
|
settingsRepository: SettingsRepository,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
|
private val environmentRepository: EnvironmentRepository,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||||
|
@ -226,14 +232,7 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
completeRegistrationData != null -> {
|
completeRegistrationData != null -> {
|
||||||
if (authRepository.activeUserId != null) {
|
handleCompleteRegistrationData(completeRegistrationData)
|
||||||
authRepository.hasPendingAccountAddition = true
|
|
||||||
}
|
|
||||||
specialCircumstanceManager.specialCircumstance =
|
|
||||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
|
||||||
completeRegistrationData = completeRegistrationData,
|
|
||||||
timestamp = clock.millis(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
autofillSaveItem != null -> {
|
autofillSaveItem != null -> {
|
||||||
|
@ -310,6 +309,47 @@ class MainViewModel @Inject constructor(
|
||||||
sendEvent(MainEvent.Recreate)
|
sendEvent(MainEvent.Recreate)
|
||||||
garbageCollectionManager.tryCollect()
|
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.
|
* Navigate to the debug menu.
|
||||||
*/
|
*/
|
||||||
data object NavigateToDebugMenu : MainEvent()
|
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.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.SendVerificationEmailRequestJson
|
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 kotlinx.serialization.json.JsonPrimitive
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
@ -79,4 +80,9 @@ interface UnauthenticatedIdentityApi {
|
||||||
suspend fun sendVerificationEmail(
|
suspend fun sendVerificationEmail(
|
||||||
@Body body: SendVerificationEmailRequestJson,
|
@Body body: SendVerificationEmailRequestJson,
|
||||||
): Result<JsonPrimitive?>
|
): 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.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
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.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.
|
* Provides an API for querying identity endpoints.
|
||||||
|
@ -72,4 +74,12 @@ interface IdentityService {
|
||||||
* Register a new account to Bitwarden using email verification flow.
|
* Register a new account to Bitwarden using email verification flow.
|
||||||
*/
|
*/
|
||||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
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.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
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.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.model.toBitwardenError
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||||
|
@ -127,4 +129,35 @@ class IdentityServiceImpl(
|
||||||
.sendVerificationEmail(body = body)
|
.sendVerificationEmail(body = body)
|
||||||
.map { it?.content }
|
.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
|
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 {
|
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.AuthState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
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.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.KnownDeviceResult
|
||||||
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.NewSsoUserResult
|
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||||
|
@ -377,4 +378,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||||
name: String,
|
name: String,
|
||||||
receiveMarketingEmails: Boolean,
|
receiveMarketingEmails: Boolean,
|
||||||
): SendVerificationEmailResult
|
): 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.TrustedDeviceUserDecryptionOptionsJson
|
||||||
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.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.AccountsService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
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.AuthState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
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.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.KnownDeviceResult
|
||||||
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.NewSsoUserResult
|
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")
|
@Suppress("CyclomaticComplexMethod")
|
||||||
private suspend fun validatePasswordAgainstPolicy(
|
private suspend fun validatePasswordAgainstPolicy(
|
||||||
password: String,
|
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
|
it?.activeAccount?.isLoggedIn == true
|
||||||
}
|
}
|
||||||
.onEach { _ ->
|
.onEach { _ ->
|
||||||
if (specialCircumstance is SpecialCircumstance.PreLogin) {
|
if (specialCircumstance is SpecialCircumstance.RegistrationEvent) {
|
||||||
specialCircumstance = null
|
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.data.autofill.model.AutofillSelectionData
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import kotlinx.parcelize.Parcelize
|
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
|
* 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
|
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||||
* cleared after a successful login.
|
* cleared after a successful login.
|
||||||
*
|
|
||||||
* @see [SpecialCircumstanceManager.clearSpecialCircumstanceAfterLogin]
|
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
sealed class PreLogin : SpecialCircumstance() {
|
sealed class RegistrationEvent : SpecialCircumstance() {
|
||||||
/**
|
/**
|
||||||
* The app was launched via AppLink in order to allow the user complete an ongoing
|
* The app was launched via AppLink in order to allow the user complete an ongoing
|
||||||
* registration.
|
* registration.
|
||||||
|
@ -106,6 +103,13 @@ sealed class SpecialCircumstance : Parcelable {
|
||||||
data class CompleteRegistration(
|
data class CompleteRegistration(
|
||||||
val completeRegistrationData: CompleteRegistrationData,
|
val completeRegistrationData: CompleteRegistrationData,
|
||||||
val timestamp: Long,
|
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.Fido2Save -> null
|
||||||
is SpecialCircumstance.Fido2Assertion -> null
|
is SpecialCircumstance.Fido2Assertion -> null
|
||||||
is SpecialCircumstance.Fido2GetCredentials -> 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.Fido2Save -> null
|
||||||
is SpecialCircumstance.Fido2Assertion -> null
|
is SpecialCircumstance.Fido2Assertion -> null
|
||||||
is SpecialCircumstance.Fido2GetCredentials -> 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
|
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
@ -52,6 +53,13 @@ fun ExpiredRegistrationLinkScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val sendCloseClicked = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = sendCloseClicked)
|
||||||
|
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -61,11 +69,7 @@ fun ExpiredRegistrationLinkScreen(
|
||||||
navigationIcon = NavigationIcon(
|
navigationIcon = NavigationIcon(
|
||||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
onNavigationIconClick = remember(viewModel) {
|
onNavigationIconClick = sendCloseClicked,
|
||||||
{
|
|
||||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
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 com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View model for the [ExpiredRegistrationLinkScreen].
|
* View model for the [ExpiredRegistrationLinkScreen].
|
||||||
*/
|
*/
|
||||||
class ExpiredRegistrationLinkViewModel @Inject constructor() :
|
@HiltViewModel
|
||||||
BaseViewModel<Unit, ExpiredRegistrationLinkEvent, ExpiredRegistrationLinkAction>(
|
class ExpiredRegistrationLinkViewModel @Inject constructor(
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
) : BaseViewModel<Unit, ExpiredRegistrationLinkEvent, ExpiredRegistrationLinkAction>(
|
||||||
initialState = Unit,
|
initialState = Unit,
|
||||||
) {
|
) {
|
||||||
override fun handleAction(action: ExpiredRegistrationLinkAction) {
|
override fun handleAction(action: ExpiredRegistrationLinkAction) {
|
||||||
|
@ -21,16 +25,28 @@ class ExpiredRegistrationLinkViewModel @Inject constructor() :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRestartRegistrationClicked() {
|
private fun handleRestartRegistrationClicked() {
|
||||||
|
resetPendingAccountAddition()
|
||||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToStartRegistration)
|
sendEvent(ExpiredRegistrationLinkEvent.NavigateToStartRegistration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGoToLoginClicked() {
|
private fun handleGoToLoginClicked() {
|
||||||
|
resetPendingAccountAddition()
|
||||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateToLogin)
|
sendEvent(ExpiredRegistrationLinkEvent.NavigateToLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCloseClicked() {
|
private fun handleCloseClicked() {
|
||||||
|
resetPendingAccountAddition()
|
||||||
sendEvent(ExpiredRegistrationLinkEvent.NavigateBack)
|
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
|
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
|
||||||
|
|
||||||
specialCircumstance is SpecialCircumstance.PreLogin.CompleteRegistration -> {
|
specialCircumstance is SpecialCircumstance.RegistrationEvent -> {
|
||||||
RootNavState.CompleteOngoingRegistration(
|
getRegistrationEventNavState(specialCircumstance)
|
||||||
email = specialCircumstance.completeRegistrationData.email,
|
|
||||||
verificationToken = specialCircumstance.completeRegistrationData.verificationToken,
|
|
||||||
fromEmail = specialCircumstance.completeRegistrationData.fromEmail,
|
|
||||||
timestamp = specialCircumstance.timestamp,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userState == null ||
|
userState == null ||
|
||||||
|
@ -141,7 +136,7 @@ class RootNavViewModel @Inject constructor(
|
||||||
null,
|
null,
|
||||||
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
|
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
|
||||||
|
|
||||||
is SpecialCircumstance.PreLogin.CompleteRegistration -> {
|
is SpecialCircumstance.RegistrationEvent -> {
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
"Special circumstance should have been already handled.",
|
"Special circumstance should have been already handled.",
|
||||||
)
|
)
|
||||||
|
@ -154,6 +149,23 @@ class RootNavViewModel @Inject constructor(
|
||||||
mutableStateFlow.update { updatedRootNavState }
|
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 {
|
private fun UserState.shouldShowRemovePassword(authState: AuthState): Boolean {
|
||||||
val isLoggedInUsingSso = (authState as? AuthState.Authenticated)
|
val isLoggedInUsingSso = (authState as? AuthState.Authenticated)
|
||||||
?.accessToken
|
?.accessToken
|
||||||
|
|
|
@ -994,4 +994,5 @@ Do you want to switch to this account?</string>
|
||||||
<string name="restart_registration">Restart registration</string>
|
<string name="restart_registration">Restart registration</string>
|
||||||
<string name="authenticator_sync">Authenticator Sync</string>
|
<string name="authenticator_sync">Authenticator Sync</string>
|
||||||
<string name="allow_bitwarden_authenticator_syncing">Allow Bitwarden Authenticator Syncing</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>
|
</resources>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.bitwarden.vault.CipherView
|
import com.bitwarden.vault.CipherView
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
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.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
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.CompleteRegistrationData
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
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.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
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.feature.settings.appearance.model.AppTheme
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||||
|
@ -81,6 +84,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
|
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
|
||||||
every { userStateFlow } returns mutableUserStateFlow
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
every { switchAccount(any()) } returns SwitchAccountResult.NoChange
|
every { switchAccount(any()) } returns SwitchAccountResult.NoChange
|
||||||
|
coEvery { validateEmailToken(any(), any()) } returns EmailTokenResult.Success
|
||||||
}
|
}
|
||||||
private val mutableVaultStateEventFlow = bufferedMutableSharedFlow<VaultStateEvent>()
|
private val mutableVaultStateEventFlow = bufferedMutableSharedFlow<VaultStateEvent>()
|
||||||
private val vaultRepository = mockk<VaultRepository> {
|
private val vaultRepository = mockk<VaultRepository> {
|
||||||
|
@ -95,6 +99,9 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
authRepository = mockAuthRepository,
|
authRepository = mockAuthRepository,
|
||||||
dispatcherManager = FakeDispatcherManager(),
|
dispatcherManager = FakeDispatcherManager(),
|
||||||
)
|
)
|
||||||
|
private val environmentRepository = mockk<EnvironmentRepository>(relaxed = true) {
|
||||||
|
every { loadEnvironmentForEmail(any()) } returns true
|
||||||
|
}
|
||||||
private val intentManager: IntentManager = mockk {
|
private val intentManager: IntentManager = mockk {
|
||||||
every { getShareDataFromIntent(any()) } returns null
|
every { getShareDataFromIntent(any()) } returns null
|
||||||
}
|
}
|
||||||
|
@ -326,9 +333,12 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@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 viewModel = createViewModel()
|
||||||
val completeRegistrationData = mockk<CompleteRegistrationData>()
|
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||||
|
every { email } returns "email"
|
||||||
|
every { verificationToken } returns "token"
|
||||||
|
}
|
||||||
val mockIntent = mockk<Intent> {
|
val mockIntent = mockk<Intent> {
|
||||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||||
every { getAutofillSaveItemOrNull() } returns null
|
every { getAutofillSaveItemOrNull() } returns null
|
||||||
|
@ -346,14 +356,173 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||||
completeRegistrationData = completeRegistrationData,
|
completeRegistrationData = completeRegistrationData,
|
||||||
timestamp = FIXED_CLOCK.millis(),
|
timestamp = FIXED_CLOCK.millis(),
|
||||||
),
|
),
|
||||||
specialCircumstanceManager.specialCircumstance,
|
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")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() {
|
fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() {
|
||||||
|
@ -804,6 +973,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
clock = FIXED_CLOCK,
|
clock = FIXED_CLOCK,
|
||||||
|
environmentRepository = environmentRepository,
|
||||||
savedStateHandle = savedStateHandle.apply {
|
savedStateHandle = savedStateHandle.apply {
|
||||||
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)
|
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.TrustedDeviceUserDecryptionOptionsJson
|
||||||
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.UserDecryptionOptionsJson
|
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.base.BaseServiceTest
|
||||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||||
|
@ -369,6 +371,77 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||||
assertTrue(result.isFailure)
|
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 {
|
companion object {
|
||||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||||
private const val REFRESH_TOKEN = "refreshToken"
|
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.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.model.UserDecryptionOptionsJson
|
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.AccountsService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
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.AuthState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
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.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.KnownDeviceResult
|
||||||
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.NewSsoUserResult
|
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 {
|
companion object {
|
||||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||||
private const val NAME = "Example Name"
|
private const val NAME = "Example Name"
|
||||||
|
|
|
@ -51,7 +51,7 @@ class SpecialCircumstanceManagerTest {
|
||||||
assertNull(awaitItem())
|
assertNull(awaitItem())
|
||||||
|
|
||||||
val preLoginSpecialCircumstance =
|
val preLoginSpecialCircumstance =
|
||||||
mockk<SpecialCircumstance.PreLogin.CompleteRegistration>()
|
mockk<SpecialCircumstance.RegistrationEvent.CompleteRegistration>()
|
||||||
|
|
||||||
specialCircumstanceManager.specialCircumstance = preLoginSpecialCircumstance
|
specialCircumstanceManager.specialCircumstance = preLoginSpecialCircumstance
|
||||||
assertEquals(preLoginSpecialCircumstance, awaitItem())
|
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.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
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.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.FakeEnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||||
|
@ -93,7 +93,8 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||||
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
||||||
}
|
}
|
||||||
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
private val mutableGeneratorResultFlow = bufferedMutableSharedFlow<GeneratorResult>()
|
||||||
private val mockCompleteRegistrationCircumstance = mockk<PreLogin.CompleteRegistration>()
|
private val mockCompleteRegistrationCircumstance =
|
||||||
|
mockk<RegistrationEvent.CompleteRegistration>()
|
||||||
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
|
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
|
||||||
every { generatorResultFlow } returns mutableGeneratorResultFlow
|
every { generatorResultFlow } returns mutableGeneratorResultFlow
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ExpiredRegistrationLinkScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
composeTestRule.setContent {
|
setContentWithBackDispatcher {
|
||||||
ExpiredRegistrationLinkScreen(
|
ExpiredRegistrationLinkScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToLogin = { onNavigateToLoginCalled = 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
|
@Test
|
||||||
fun `CloseClicked sends NavigateBack action`() {
|
fun `CloseClicked sends NavigateBack action`() {
|
||||||
composeTestRule
|
composeTestRule
|
||||||
|
|
|
@ -1,37 +1,51 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
package com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class ExpiredRegistrationLinkViewModelTest : BaseViewModelTest() {
|
class ExpiredRegistrationLinkViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val authRepository = mockk<AuthRepository>(relaxed = true)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CloseClicked sends NavigateBack event`() = runTest {
|
fun `CloseClicked sends NavigateBack event and resets pending account addition`() = runTest {
|
||||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
viewModel.trySendAction(ExpiredRegistrationLinkAction.CloseClicked)
|
||||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateBack, awaitItem())
|
assertEquals(ExpiredRegistrationLinkEvent.NavigateBack, awaitItem())
|
||||||
}
|
}
|
||||||
|
verify { authRepository.hasPendingAccountAddition = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `RestartRegistrationClicked sends NavigateToStartRegistration event`() = runTest {
|
fun `RestartRegistrationClicked sends NavigateToStartRegistration event and resets pending account addition`() =
|
||||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
runTest {
|
||||||
viewModel.eventFlow.test {
|
val viewModel = createViewModel()
|
||||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked)
|
viewModel.eventFlow.test {
|
||||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToStartRegistration, awaitItem())
|
viewModel.trySendAction(ExpiredRegistrationLinkAction.RestartRegistrationClicked)
|
||||||
|
assertEquals(ExpiredRegistrationLinkEvent.NavigateToStartRegistration, awaitItem())
|
||||||
|
}
|
||||||
|
verify { authRepository.hasPendingAccountAddition = true }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GoToLoginClicked sends NavigateToLogin event`() = runTest {
|
fun `GoToLoginClicked sends NavigateToLogin event and resets pending account addition`() =
|
||||||
val viewModel = ExpiredRegistrationLinkViewModel()
|
runTest {
|
||||||
viewModel.eventFlow.test {
|
val viewModel = createViewModel()
|
||||||
viewModel.trySendAction(ExpiredRegistrationLinkAction.GoToLoginClicked)
|
viewModel.eventFlow.test {
|
||||||
assertEquals(ExpiredRegistrationLinkEvent.NavigateToLogin, awaitItem())
|
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
|
every { authRepository.hasPendingAccountAddition } returns false
|
||||||
|
|
||||||
specialCircumstanceManager.specialCircumstance =
|
specialCircumstanceManager.specialCircumstance =
|
||||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||||
CompleteRegistrationData(
|
CompleteRegistrationData(
|
||||||
email = "example@email.com",
|
email = "example@email.com",
|
||||||
verificationToken = "token",
|
verificationToken = "token",
|
||||||
|
@ -690,7 +690,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
every { authRepository.hasPendingAccountAddition } returns true
|
every { authRepository.hasPendingAccountAddition } returns true
|
||||||
|
|
||||||
specialCircumstanceManager.specialCircumstance =
|
specialCircumstanceManager.specialCircumstance =
|
||||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||||
CompleteRegistrationData(
|
CompleteRegistrationData(
|
||||||
email = "example@email.com",
|
email = "example@email.com",
|
||||||
verificationToken = "token",
|
verificationToken = "token",
|
||||||
|
@ -740,7 +740,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
every { authRepository.hasPendingAccountAddition } returns true
|
every { authRepository.hasPendingAccountAddition } returns true
|
||||||
|
|
||||||
specialCircumstanceManager.specialCircumstance =
|
specialCircumstanceManager.specialCircumstance =
|
||||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||||
CompleteRegistrationData(
|
CompleteRegistrationData(
|
||||||
email = "example@email.com",
|
email = "example@email.com",
|
||||||
verificationToken = "token",
|
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
|
@Test
|
||||||
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
||||||
mutableUserStateFlow.tryEmit(
|
mutableUserStateFlow.tryEmit(
|
||||||
|
|
Loading…
Add table
Reference in a new issue