From 5c8f5670e4505a9d81709449c039deea1306262c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Wed, 7 May 2025 22:25:32 +0100 Subject: [PATCH] [PM-21203] Old user migration login error. (#5136) --- .../auth/repository/AuthRepositoryImpl.kt | 6 +- .../data/auth/repository/model/LoginResult.kt | 5 + .../EnterpriseSignOnViewModel.kt | 17 ++ .../ui/auth/feature/login/LoginViewModel.kt | 23 +- .../LoginWithDeviceViewModel.kt | 1 + .../twofactorlogin/TwoFactorLoginViewModel.kt | 3 +- app/src/main/res/values/strings.xml | 1 + .../auth/repository/AuthRepositoryTest.kt | 41 ++++ .../EnterpriseSignOnViewModelTest.kt | 227 ++++++++++++++++++ .../auth/feature/login/LoginViewModelTest.kt | 141 +++++++++++ .../network/model/GetTokenResponseJson.kt | 11 + 11 files changed, 473 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 2c6de36c1a..973584dc95 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -1613,7 +1613,7 @@ class AuthRepositoryImpl( * A helper function to extract the common logic of logging in through * any of the available methods. */ - @Suppress("LongMethod") + @Suppress("LongMethod", "MaxLineLength") private suspend fun loginCommon( email: String, password: String? = null, @@ -1677,6 +1677,10 @@ class AuthRepositoryImpl( error = loginResponse.errorMessage, ) + is GetTokenResponseJson.Invalid.InvalidType.EncryptionKeyMigrationRequired -> { + LoginResult.EncryptionKeyMigrationRequired + } + is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> { LoginResult.Error( errorMessage = loginResponse.errorMessage, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt index 729aaea965..91656b6dfa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt @@ -14,6 +14,11 @@ sealed class LoginResult { */ data class CaptchaRequired(val captchaId: String) : LoginResult() + /** + * Encryption key migration is required. + */ + data object EncryptionKeyMigrationRequired : LoginResult() + /** * Two-factor verification is required. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt index 1841017d54..13337805a7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt @@ -5,6 +5,7 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.data.repository.util.baseIdentityUrl +import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R @@ -22,6 +23,7 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository +import com.x8bit.bitwarden.data.platform.util.toUriOrNull import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository import com.x8bit.bitwarden.data.tools.generator.repository.utils.generateRandomString import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -154,6 +156,7 @@ class EnterpriseSignOnViewModel @Inject constructor( prevalidateSso() } + @Suppress("LongMethod") private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) { when (val loginResult = action.loginResult) { is LoginResult.CaptchaRequired -> { @@ -197,6 +200,20 @@ class EnterpriseSignOnViewModel @Inject constructor( ) } + is LoginResult.EncryptionKeyMigrationRequired -> { + val vaultUrl = + environmentRepository + .environment + .environmentUrlData + .baseWebVaultUrlOrDefault + + showError( + message = R.string + .this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText(vaultUrl.toUriOrNull()?.host ?: vaultUrl), + ) + } + LoginResult.CertificateError -> { showError(message = R.string.we_couldnt_verify_the_servers_certificate.asText()) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt index 2c755e20f1..2b7d447df8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt @@ -6,6 +6,7 @@ import android.net.Uri import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R @@ -16,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository +import com.x8bit.bitwarden.data.platform.util.toUriOrNull import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary @@ -37,7 +39,7 @@ private const val KEY_STATE = "state" @HiltViewModel class LoginViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - environmentRepository: EnvironmentRepository, + private val environmentRepository: EnvironmentRepository, private val authRepository: AuthRepository, private val vaultRepository: VaultRepository, ) : BaseViewModel( @@ -166,6 +168,25 @@ class LoginViewModel @Inject constructor( ) } + is LoginResult.EncryptionKeyMigrationRequired -> { + val vaultUrl = + environmentRepository + .environment + .environmentUrlData + .baseWebVaultUrlOrDefault + + mutableStateFlow.update { + it.copy( + dialogState = LoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string + .this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText(vaultUrl.toUriOrNull()?.host ?: vaultUrl), + ), + ) + } + } + is LoginResult.TwoFactorRequired -> { mutableStateFlow.update { it.copy(dialogState = null) } sendEvent( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt index 9c53ab10ad..3c94f7ba4a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt @@ -286,6 +286,7 @@ class LoginWithDeviceViewModel @Inject constructor( // NO-OP: This result should not be possible here is LoginResult.ConfirmKeyConnectorDomain -> Unit + LoginResult.EncryptionKeyMigrationRequired -> Unit } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index 398918fdae..d5921eeeb5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt @@ -282,7 +282,7 @@ class TwoFactorLoginViewModel @Inject constructor( /** * Handle the login result. */ - @Suppress("MaxLineLength") + @Suppress("MaxLineLength", "LongMethod") private fun handleReceiveLoginResult(action: TwoFactorLoginAction.Internal.ReceiveLoginResult) { // Dismiss the loading overlay. mutableStateFlow.update { it.copy(dialogState = null) } @@ -353,6 +353,7 @@ class TwoFactorLoginViewModel @Inject constructor( // NO-OP: This result should not be possible here is LoginResult.ConfirmKeyConnectorDomain -> Unit + LoginResult.EncryptionKeyMigrationRequired -> Unit } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21bf84c0a9..115bb74734 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1265,4 +1265,5 @@ Do you want to switch to this account? Unable to read certificate. Cannot delete your account This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details. + This account will soon be deleted. Log in at %1$s to continue using Bitwarden. diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 0cc10b53be..8953f5fad9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -2392,6 +2392,47 @@ class AuthRepositoryTest { ) } + @Test + @Suppress("MaxLineLength") + fun `login get token returns invalid request should return EncryptionKeyMigrationRequired`() = + runTest { + coEvery { identityService.preLogin(EMAIL) } returns PRE_LOGIN_SUCCESS.asSuccess() + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns GetTokenResponseJson + .Invalid( + errorModel = GetTokenResponseJson.Invalid.ErrorModel( + errorMessage = + "Encryption key migration is required. Please log in to the web vault at", + ), + ) + .asSuccess() + + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.EncryptionKeyMigrationRequired, result) + assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value) + coVerify { + identityService.preLogin(email = EMAIL) + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } + } + @Test fun `login with device get token fails should return Error with no message`() = runTest { val error = Throwable("Fail!") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt index 4592984413..a2c9085d5b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt @@ -4,6 +4,8 @@ import android.net.Uri import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow +import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson +import com.bitwarden.data.repository.model.Environment import com.bitwarden.network.model.VerifiedOrganizationDomainSsoDetailsResponse import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R @@ -461,6 +463,231 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { } } + @Test + @Suppress("MaxLineLength") + fun `ssoCallbackResultFlow Success with same state with login EncryptionKeyMigrationRequired should update dialogState with web vault url`() = + runTest { + val orgIdentifier = "Bitwarden" + coEvery { + authRepository.login(any(), any(), any(), any(), any(), any()) + } returns LoginResult.EncryptionKeyMigrationRequired + + environmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "", + webVault = "vault.bitwarden.com", + ), + ) + + val viewModel = createViewModel( + ssoData = DEFAULT_SSO_DATA, + ) + val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn") + + viewModel.stateFlow.test { + assertEquals( + DEFAULT_STATE, + awaitItem(), + ) + + viewModel.trySendAction( + EnterpriseSignOnAction.OrgIdentifierInputChange(orgIdentifier), + ) + + assertEquals( + DEFAULT_STATE.copy( + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + mutableSsoCallbackResultFlow.tryEmit(ssoCallbackResult) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Loading( + R.string.logging_in.asText(), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("vault.bitwarden.com"), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + } + + coVerify(exactly = 1) { + authRepository.login( + email = "test@gmail.com", + ssoCode = "lmn", + ssoCodeVerifier = "def", + ssoRedirectUri = "bitwarden://sso-callback", + captchaToken = null, + organizationIdentifier = orgIdentifier, + ) + } + } + + @Test + @Suppress("MaxLineLength") + fun `ssoCallbackResultFlow Success with same state with login EncryptionKeyMigrationRequired should update dialogState with base url`() = + runTest { + val orgIdentifier = "Bitwarden" + coEvery { + authRepository.login(any(), any(), any(), any(), any(), any()) + } returns LoginResult.EncryptionKeyMigrationRequired + + environmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "base.bitwarden.com", + webVault = "", + ), + ) + + val viewModel = createViewModel( + ssoData = DEFAULT_SSO_DATA, + ) + val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn") + + viewModel.stateFlow.test { + assertEquals( + DEFAULT_STATE, + awaitItem(), + ) + + viewModel.trySendAction( + EnterpriseSignOnAction.OrgIdentifierInputChange(orgIdentifier), + ) + + assertEquals( + DEFAULT_STATE.copy( + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + mutableSsoCallbackResultFlow.tryEmit(ssoCallbackResult) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Loading( + R.string.logging_in.asText(), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("base.bitwarden.com"), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + } + + coVerify(exactly = 1) { + authRepository.login( + email = "test@gmail.com", + ssoCode = "lmn", + ssoCodeVerifier = "def", + ssoRedirectUri = "bitwarden://sso-callback", + captchaToken = null, + organizationIdentifier = orgIdentifier, + ) + } + } + + @Test + @Suppress("MaxLineLength") + fun `ssoCallbackResultFlow Success with same state with login EncryptionKeyMigrationRequired should update dialogState with default url`() = + runTest { + val orgIdentifier = "Bitwarden" + coEvery { + authRepository.login(any(), any(), any(), any(), any(), any()) + } returns LoginResult.EncryptionKeyMigrationRequired + + environmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "", + webVault = "", + ), + ) + + val viewModel = createViewModel( + ssoData = DEFAULT_SSO_DATA, + ) + val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn") + + viewModel.stateFlow.test { + assertEquals( + DEFAULT_STATE, + awaitItem(), + ) + + viewModel.trySendAction( + EnterpriseSignOnAction.OrgIdentifierInputChange(orgIdentifier), + ) + + assertEquals( + DEFAULT_STATE.copy( + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + mutableSsoCallbackResultFlow.tryEmit(ssoCallbackResult) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Loading( + R.string.logging_in.asText(), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("vault.bitwarden.com"), + ), + orgIdentifierInput = orgIdentifier, + ), + awaitItem(), + ) + } + + coVerify(exactly = 1) { + authRepository.login( + email = "test@gmail.com", + ssoCode = "lmn", + ssoCodeVerifier = "def", + ssoRedirectUri = "bitwarden://sso-callback", + captchaToken = null, + organizationIdentifier = orgIdentifier, + ) + } + } + @Suppress("MaxLineLength") @Test fun `ssoCallbackResultFlow Success with same state with login CertificateError should show loading dialog then show certificate error dialog`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index ef9f3fd682..d46d2a2a8d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -38,6 +38,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +@Suppress("LargeClass") class LoginViewModelTest : BaseViewModelTest() { private val mutableCaptchaTokenResultFlow = @@ -333,6 +334,146 @@ class LoginViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `LoginButtonClick login returns EncryptionKeyMigrationRequired should update errorDialogState with webVault url`() = + runTest { + coEvery { + authRepository.login( + email = EMAIL, + password = "", + captchaToken = null, + ) + } returns LoginResult.EncryptionKeyMigrationRequired + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "", + webVault = "vault.bitwarden.com", + ), + ) + val viewModel = createViewModel() + val defaultSelfHostedState = DEFAULT_STATE.copy(environmentLabel = "") + viewModel.stateFlow.test { + assertEquals(defaultSelfHostedState, awaitItem()) + viewModel.trySendAction(LoginAction.LoginButtonClick) + assertEquals( + defaultSelfHostedState.copy( + environmentLabel = "", + dialogState = LoginState.DialogState.Loading( + message = R.string.logging_in.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + defaultSelfHostedState.copy( + environmentLabel = "", + dialogState = LoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("vault.bitwarden.com"), + ), + ), + awaitItem(), + ) + } + coVerify { + authRepository.login(email = EMAIL, password = "", captchaToken = null) + } + } + + @Suppress("MaxLineLength") + @Test + fun `LoginButtonClick login returns EncryptionKeyMigrationRequired should update errorDialogState with base url`() = + runTest { + coEvery { + authRepository.login( + email = EMAIL, + password = "", + captchaToken = null, + ) + } returns LoginResult.EncryptionKeyMigrationRequired + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "base.bitwarden.com", + webVault = "", + ), + ) + val viewModel = createViewModel() + val defaultSelfHostedState = DEFAULT_STATE.copy(environmentLabel = "") + viewModel.stateFlow.test { + assertEquals(defaultSelfHostedState, awaitItem()) + viewModel.trySendAction(LoginAction.LoginButtonClick) + assertEquals( + defaultSelfHostedState.copy( + dialogState = LoginState.DialogState.Loading( + message = R.string.logging_in.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + defaultSelfHostedState.copy( + dialogState = LoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("base.bitwarden.com"), + ), + ), + awaitItem(), + ) + } + coVerify { + authRepository.login(email = EMAIL, password = "", captchaToken = null) + } + } + + @Suppress("MaxLineLength") + @Test + fun `LoginButtonClick login returns EncryptionKeyMigrationRequired should update errorDialogState with default url`() = + runTest { + coEvery { + authRepository.login( + email = EMAIL, + password = "", + captchaToken = null, + ) + } returns LoginResult.EncryptionKeyMigrationRequired + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "", + webVault = "", + ), + ) + val viewModel = createViewModel() + val defaultSelfHostedState = DEFAULT_STATE.copy(environmentLabel = "") + viewModel.stateFlow.test { + assertEquals(defaultSelfHostedState, awaitItem()) + viewModel.trySendAction(LoginAction.LoginButtonClick) + assertEquals( + defaultSelfHostedState.copy( + dialogState = LoginState.DialogState.Loading( + message = R.string.logging_in.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + defaultSelfHostedState.copy( + dialogState = LoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden + .asText("vault.bitwarden.com"), + ), + ), + awaitItem(), + ) + } + coVerify { + authRepository.login(email = EMAIL, password = "", captchaToken = null) + } + } + @Suppress("MaxLineLength") @Test fun `LoginButtonClick login returns CertificateError should update errorDialogState`() = diff --git a/network/src/main/kotlin/com/bitwarden/network/model/GetTokenResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/GetTokenResponseJson.kt index 13633ad15b..346d60b4b8 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/GetTokenResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/GetTokenResponseJson.kt @@ -119,6 +119,11 @@ sealed class GetTokenResponseJson { */ data object NewDeviceVerification : InvalidType() + /** + * Represents an invalid response indicating that a new device verification is required. + */ + data object EncryptionKeyMigrationRequired : InvalidType() + /** * Represents generic invalid response */ @@ -128,6 +133,12 @@ sealed class GetTokenResponseJson { val invalidType: InvalidType get() = if (errorMessage?.lowercase() == "new device verification required") { InvalidType.NewDeviceVerification + } else if (errorMessage + ?.lowercase() + ?.contains( + "encryption key migration is required. please log in to the web vault at", + ) == true) { + InvalidType.EncryptionKeyMigrationRequired } else { InvalidType.GenericInvalid }