From 5faa30e2f29c4aa067cf7d5d5b32f44b592c80d5 Mon Sep 17 00:00:00 2001
From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Date: Thu, 17 Oct 2024 11:11:13 -0400
Subject: [PATCH] [PM-13396] Show error when logging into an unofficial
Bitwarden server (#4088)
---
.../auth/repository/AuthRepositoryImpl.kt | 9 +-
.../repository/di/AuthRepositoryModule.kt | 3 +
.../data/auth/repository/model/LoginResult.kt | 5 +
.../datasource/disk/model/ServerConfig.kt | 8 +-
.../EnterpriseSignOnViewModel.kt | 12 ++
.../ui/auth/feature/login/LoginViewModel.kt | 12 ++
.../LoginWithDeviceViewModel.kt | 13 ++
.../twofactorlogin/TwoFactorLoginViewModel.kt | 13 ++
app/src/main/res/values/strings.xml | 3 +-
.../auth/repository/AuthRepositoryTest.kt | 125 +++++++++++++-----
.../EnterpriseSignOnViewModelTest.kt | 68 +++++++++-
.../auth/feature/login/LoginViewModelTest.kt | 38 ++++++
.../LoginWithDeviceViewModelTest.kt | 63 +++++++++
.../TwoFactorLoginViewModelTest.kt | 60 +++++++++
14 files changed, 396 insertions(+), 36 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 cf09d1af0..5a978ad26 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
@@ -94,6 +94,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
+import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -153,6 +154,7 @@ class AuthRepositoryImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
+ private val configDiskSource: ConfigDiskSource,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
@@ -1472,7 +1474,12 @@ class AuthRepositoryImpl(
captchaToken = captchaToken,
)
.fold(
- onFailure = { LoginResult.Error(errorMessage = null) },
+ onFailure = {
+ when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
+ false -> LoginResult.UnofficialServerError
+ else -> LoginResult.Error(errorMessage = null)
+ }
+ },
onSuccess = { loginResponse ->
when (loginResponse) {
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt
index e44fc53eb..c7a67fc48 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
+import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -45,6 +46,7 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
+ configDiskSource: ConfigDiskSource,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
settingsRepository: SettingsRepository,
@@ -64,6 +66,7 @@ object AuthRepositoryModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
+ configDiskSource = configDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatcherManager,
environmentRepository = environmentRepository,
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 f3d3bbe5e..6794ccf77 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
@@ -23,4 +23,9 @@ sealed class LoginResult {
* There was an error logging in.
*/
data class Error(val errorMessage: String?) : LoginResult()
+
+ /**
+ * There was an error while logging into an unofficial Bitwarden server.
+ */
+ data object UnofficialServerError : LoginResult()
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt
index 3686d4c62..e1935686b 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt
@@ -19,4 +19,10 @@ data class ServerConfig(
@SerialName("serverData")
val serverData: ConfigResponseJson,
-)
+) {
+ /**
+ * Whether the server is an official Bitwarden server or not.
+ */
+ val isOfficialBitwardenServer: Boolean
+ get() = serverData.server == null
+}
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 3fb5547a7..243c61327 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
@@ -138,6 +138,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
prevalidateSso()
}
+ @Suppress("MaxLineLength")
private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) {
when (val loginResult = action.loginResult) {
is LoginResult.CaptchaRequired -> {
@@ -160,6 +161,17 @@ class EnterpriseSignOnViewModel @Inject constructor(
}
}
+ is LoginResult.UnofficialServerError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = EnterpriseSignOnState.DialogState.Error(
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
+ .asText(),
+ ),
+ )
+ }
+ }
+
is LoginResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
authRepository.rememberedOrgIdentifier = state.orgIdentifierInput
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 8218ce66f..0e3829221 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
@@ -143,6 +143,7 @@ class LoginViewModel @Inject constructor(
}
}
+ @Suppress("MaxLineLength")
private fun handleReceiveLoginResult(action: LoginAction.Internal.ReceiveLoginResult) {
when (val loginResult = action.loginResult) {
is LoginResult.CaptchaRequired -> {
@@ -176,6 +177,17 @@ class LoginViewModel @Inject constructor(
}
}
+ LoginResult.UnofficialServerError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = LoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
+ ),
+ )
+ }
+ }
+
is LoginResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
}
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 cbc952071..14bacb994 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
@@ -225,6 +225,7 @@ class LoginWithDeviceViewModel @Inject constructor(
}
}
+ @Suppress("MaxLineLength")
private fun handleReceiveLoginResult(
action: LoginWithDeviceAction.Internal.ReceiveLoginResult,
) {
@@ -261,6 +262,18 @@ class LoginWithDeviceViewModel @Inject constructor(
}
}
+ is LoginResult.UnofficialServerError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = LoginWithDeviceState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
+ .asText(),
+ ),
+ )
+ }
+ }
+
is LoginResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
}
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 4d71bf2ae..71672d79c 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
@@ -278,6 +278,7 @@ class TwoFactorLoginViewModel @Inject constructor(
/**
* Handle the login result.
*/
+ @Suppress("MaxLineLength")
private fun handleReceiveLoginResult(action: TwoFactorLoginAction.Internal.ReceiveLoginResult) {
// Dismiss the loading overlay.
mutableStateFlow.update { it.copy(dialogState = null) }
@@ -308,6 +309,18 @@ class TwoFactorLoginViewModel @Inject constructor(
}
}
+ is LoginResult.UnofficialServerError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
+ .asText(),
+ ),
+ )
+ }
+ }
+
// NO-OP: Let the auth flow handle navigation after this.
is LoginResult.Success -> Unit
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 185f2377a..0766e24d1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1054,8 +1054,9 @@ Do you want to switch to this account?
then Done
For your security, be sure to delete your saved password file.
delete your saved password file.
- Need help? Checkout out import help.
+ Need help? Checkout out import help.
import help
Save the exported file somewhere on your computer you can find easily.
Save the exported file
+ This is not a recognized Bitwarden server. You may need to check with your provider or update your server.
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 de62924d5..60ffe0796 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
@@ -98,6 +98,9 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
+import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
+import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeConfigDiskSource
+import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -208,6 +211,7 @@ class AuthRepositoryTest {
)
.asSuccess()
}
+ private val configDiskSource = FakeConfigDiskSource()
private val vaultSdkSource = mockk {
coEvery {
getAuthRequestKey(
@@ -249,6 +253,7 @@ class AuthRepositoryTest {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = fakeAuthDiskSource,
+ configDiskSource = configDiskSource,
environmentRepository = fakeEnvironmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
@@ -1430,38 +1435,65 @@ class AuthRepositoryTest {
coVerify { identityService.preLogin(email = EMAIL) }
}
+ @Suppress("MaxLineLength")
@Test
- fun `login get token fails should return Error with no message`() = runTest {
- coEvery {
- identityService.preLogin(email = 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 RuntimeException().asFailure()
- val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
- assertEquals(LoginResult.Error(errorMessage = null), result)
- assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
- coVerify { identityService.preLogin(email = EMAIL) }
- coVerify {
- identityService.getToken(
- email = EMAIL,
- authModel = IdentityTokenAuthModel.MasterPassword(
- username = EMAIL,
- password = PASSWORD_HASH,
- ),
- captchaToken = null,
- uniqueAppId = UNIQUE_APP_ID,
- )
+ fun `login get token fails should return Error with no message when server is an official Bitwarden server`() =
+ runTest {
+ coEvery {
+ identityService.preLogin(email = 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 RuntimeException().asFailure()
+ val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
+ assertEquals(LoginResult.Error(errorMessage = null), result)
+ assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
+ coVerify { identityService.preLogin(email = EMAIL) }
+ coVerify {
+ identityService.getToken(
+ email = EMAIL,
+ authModel = IdentityTokenAuthModel.MasterPassword(
+ username = EMAIL,
+ password = PASSWORD_HASH,
+ ),
+ captchaToken = null,
+ uniqueAppId = UNIQUE_APP_ID,
+ )
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `login get token fails should return UnofficialServerError when server is an unofficial Bitwarden server`() =
+ runTest {
+ configDiskSource.serverConfig = SERVER_CONFIG_UNOFFICIAL
+ coEvery {
+ identityService.preLogin(email = 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 RuntimeException().asFailure()
+ val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
+ assertEquals(LoginResult.UnofficialServerError, result)
+ assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
+ coVerify { identityService.preLogin(email = EMAIL) }
}
- }
@Test
fun `login get token returns Invalid should return Error with correct message`() = runTest {
@@ -6513,7 +6545,36 @@ class AuthRepositoryTest {
)
private val FIRST_TIME_STATE = UserState.FirstTimeState(
- showImportLoginsCard = true,
-)
+ showImportLoginsCard = true,
+ )
+
+ private val SERVER_CONFIG_DEFAULT = ServerConfig(
+ lastSync = 0L,
+ serverData = ConfigResponseJson(
+ type = "mockType",
+ version = "mockVersion",
+ gitHash = "mockGitHash",
+ server = null,
+ environment = ConfigResponseJson.EnvironmentJson(
+ cloudRegion = "mockCloudRegion",
+ vaultUrl = "mockVaultUrl",
+ apiUrl = "mockApiUrl",
+ identityUrl = "mockIdentityUrl",
+ notificationsUrl = "mockNotificationsUrl",
+ ssoUrl = "mockSsoUrl",
+ ),
+ featureStates = emptyMap(),
+ ),
+ )
+
+ private val SERVER_CONFIG_UNOFFICIAL = SERVER_CONFIG_DEFAULT
+ .copy(
+ serverData = SERVER_CONFIG_DEFAULT.serverData.copy(
+ server = ConfigResponseJson.ServerJson(
+ name = "mockUnofficialServerName",
+ url = "mockUnofficialServerUrl",
+ ),
+ ),
+ )
}
}
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 bd7317770..0ffffe07b 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
@@ -304,7 +304,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
- fun `ssoCallbackResultFlow Success with same state with login Error should show loading dialog then show an error`() =
+ fun `ssoCallbackResultFlow Success with same state with login Error should show loading dialog then show an error when server is an official Bitwarden server`() =
runTest {
val orgIdentifier = "Bitwarden"
coEvery {
@@ -368,6 +368,72 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ssoCallbackResultFlow Success with same state with login UnofficialServerError should show loading dialog then show unofficial error when Bitwarden server is unofficial`() =
+ runTest {
+ val orgIdentifier = "Bitwarden"
+ coEvery {
+ authRepository.login(any(), any(), any(), any(), any(), any())
+ } returns LoginResult.UnofficialServerError
+
+ 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(
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
+ ),
+ 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 Success should show loading dialog, hide it, and save org identifier`() =
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 92caa280c..c05baa675 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
@@ -280,6 +280,44 @@ class LoginViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `LoginButtonClick login returns UnofficialServerError should update errorDialogState`() =
+ runTest {
+ coEvery {
+ authRepository.login(
+ email = EMAIL,
+ password = "",
+ captchaToken = null,
+ )
+ } returns LoginResult.UnofficialServerError
+ val viewModel = createViewModel()
+ viewModel.stateFlow.test {
+ assertEquals(DEFAULT_STATE, awaitItem())
+ viewModel.trySendAction(LoginAction.LoginButtonClick)
+ assertEquals(
+ DEFAULT_STATE.copy(
+ dialogState = LoginState.DialogState.Loading(
+ message = R.string.logging_in.asText(),
+ ),
+ ),
+ awaitItem(),
+ )
+ assertEquals(
+ DEFAULT_STATE.copy(
+ dialogState = LoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
+ ),
+ ),
+ awaitItem(),
+ )
+ }
+ coVerify {
+ authRepository.login(email = EMAIL, password = "", captchaToken = null)
+ }
+ }
+
@Test
fun `LoginButtonClick login returns success should update loadingDialogState`() = runTest {
coEvery {
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
index 51e6f4cde..8b37fd22c 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
@@ -362,6 +362,69 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on createAuthRequestWithUpdates Success and login UnofficialServerError should should update the state`() =
+ runTest {
+ coEvery {
+ authRepository.login(
+ email = EMAIL,
+ requestId = DEFAULT_LOGIN_DATA.requestId,
+ accessCode = DEFAULT_LOGIN_DATA.accessCode,
+ asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
+ requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
+ masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
+ captchaToken = null,
+ )
+ } returns LoginResult.UnofficialServerError
+ val viewModel = createViewModel()
+ viewModel.eventFlow.test {
+ viewModel.stateFlow.test {
+ assertEquals(DEFAULT_STATE, awaitItem())
+ mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
+ CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
+ )
+ assertEquals(
+ DEFAULT_STATE.copy(
+ viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
+ fingerprintPhrase = "",
+ ),
+ loginData = DEFAULT_LOGIN_DATA,
+ dialogState = LoginWithDeviceState.DialogState.Loading(
+ message = R.string.logging_in.asText(),
+ ),
+ ),
+ awaitItem(),
+ )
+ assertEquals(
+ DEFAULT_STATE.copy(
+ viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
+ fingerprintPhrase = "",
+ ),
+ dialogState = LoginWithDeviceState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
+ ),
+ loginData = DEFAULT_LOGIN_DATA,
+ ),
+ awaitItem(),
+ )
+ }
+ }
+
+ coVerify(exactly = 1) {
+ authRepository.login(
+ email = EMAIL,
+ requestId = AUTH_REQUEST.id,
+ accessCode = AUTH_REQUEST_RESPONSE.accessCode,
+ asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
+ requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
+ masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
+ captchaToken = null,
+ )
+ }
+ }
+
@Test
fun `on captchaTokenResultFlow missing token should should display error dialog`() = runTest {
val viewModel = createViewModel()
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
index 8b09f44b5..a5063c496 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
@@ -646,6 +646,66 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ContinueButtonClick login returns UnofficialServerError should update dialogState`() =
+ runTest {
+ coEvery {
+ authRepository.login(
+ email = DEFAULT_EMAIL_ADDRESS,
+ password = DEFAULT_PASSWORD,
+ twoFactorData = TwoFactorDataModel(
+ code = "",
+ method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
+ remember = false,
+ ),
+ captchaToken = null,
+ orgIdentifier = DEFAULT_ORG_IDENTIFIER,
+ )
+ } returns LoginResult.UnofficialServerError
+
+ val viewModel = createViewModel()
+ viewModel.stateFlow.test {
+ assertEquals(DEFAULT_STATE, awaitItem())
+
+ viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
+ assertEquals(
+ DEFAULT_STATE.copy(
+ dialogState = TwoFactorLoginState.DialogState.Loading(
+ message = R.string.logging_in.asText(),
+ ),
+ ),
+ awaitItem(),
+ )
+
+ assertEquals(
+ DEFAULT_STATE.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
+ ),
+ ),
+ awaitItem(),
+ )
+
+ viewModel.trySendAction(TwoFactorLoginAction.DialogDismiss)
+ assertEquals(DEFAULT_STATE, awaitItem())
+ }
+ coVerify {
+ authRepository.login(
+ email = DEFAULT_EMAIL_ADDRESS,
+ password = DEFAULT_PASSWORD,
+ twoFactorData = TwoFactorDataModel(
+ code = "",
+ method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
+ remember = false,
+ ),
+ captchaToken = null,
+ orgIdentifier = DEFAULT_ORG_IDENTIFIER,
+ )
+ }
+ }
+
@Test
fun `RememberMeToggle should update the state`() {
val viewModel = createViewModel()