From 596039ab213d4e1b76e1f00798f109da261e5050 Mon Sep 17 00:00:00 2001
From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com>
Date: Tue, 17 Dec 2024 14:36:46 -0500
Subject: [PATCH] =?UTF-8?q?PM-15890=20=F0=9F=8D=92=20=20TLS=20related=20er?=
=?UTF-8?q?ror=20propagation=20(#4484)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../auth/repository/AuthRepositoryImpl.kt | 17 +++--
.../data/auth/repository/model/LoginResult.kt | 5 ++
.../datasource/network/util/NetworkUtils.kt | 11 +++
.../EnterpriseSignOnViewModel.kt | 11 +++
.../ui/auth/feature/login/LoginViewModel.kt | 13 +++-
.../LoginWithDeviceViewModel.kt | 13 +++-
.../twofactorlogin/TwoFactorLoginViewModel.kt | 10 +++
app/src/main/res/values/strings.xml | 2 +-
.../auth/repository/AuthRepositoryTest.kt | 38 +++++++++++
.../network/util/NetworkUtilsTest.kt | 35 ++++++++++
.../EnterpriseSignOnViewModelTest.kt | 67 ++++++++++++++++++
.../auth/feature/login/LoginViewModelTest.kt | 38 +++++++++++
.../LoginWithDeviceViewModelTest.kt | 68 +++++++++++++++++++
.../TwoFactorLoginViewModelTest.kt | 60 ++++++++++++++++
14 files changed, 381 insertions(+), 7 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 5501bb0deb..6b4764cdbf 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.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
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.datasource.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
@@ -625,7 +626,12 @@ class AuthRepositoryImpl(
)
}
.fold(
- onFailure = { LoginResult.Error(errorMessage = null) },
+ onFailure = { throwable ->
+ when {
+ throwable.isSslHandShakeError() -> LoginResult.CertificateError
+ else -> LoginResult.Error(errorMessage = null)
+ }
+ },
onSuccess = { it },
)
@@ -1508,9 +1514,12 @@ class AuthRepositoryImpl(
captchaToken = captchaToken,
)
.fold(
- onFailure = {
- when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
- false -> LoginResult.UnofficialServerError
+ onFailure = { throwable ->
+ when {
+ throwable.isSslHandShakeError() -> LoginResult.CertificateError
+ configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
+ LoginResult.UnofficialServerError
+ }
else -> LoginResult.Error(errorMessage = null)
}
},
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 6794ccf77e..45042e2ab0 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
@@ -28,4 +28,9 @@ sealed class LoginResult {
* There was an error while logging into an unofficial Bitwarden server.
*/
data object UnofficialServerError : LoginResult()
+
+ /**
+ * There was an error in validating the certificate chain for the server
+ */
+ data object CertificateError : LoginResult()
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt
index b384cdaa4d..4e1941b81e 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util
import okio.ByteString.Companion.decodeBase64
import java.net.UnknownHostException
import java.nio.charset.Charset
+import java.security.cert.CertPathValidatorException
import java.util.Base64
+import javax.net.ssl.SSLHandshakeException
/**
* Base 64 encode the string as well as make special modifications required by the backend:
@@ -41,3 +43,12 @@ fun Throwable?.isNoConnectionError(): Boolean {
return this is UnknownHostException ||
this?.cause?.isNoConnectionError() ?: false
}
+
+/**
+ * Returns true if the throwable represents a SSL handshake error.
+ */
+fun Throwable?.isSslHandShakeError(): Boolean {
+ return this is SSLHandshakeException ||
+ this is CertPathValidatorException ||
+ this?.cause?.isSslHandShakeError() ?: false
+}
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 1ae84ce2aa..f0684a204c 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
@@ -194,6 +194,17 @@ class EnterpriseSignOnViewModel @Inject constructor(
),
)
}
+
+ LoginResult.CertificateError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = EnterpriseSignOnState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ 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 0e38292219..e1b7f82118 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
@@ -191,6 +191,17 @@ class LoginViewModel @Inject constructor(
is LoginResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
}
+
+ LoginResult.CertificateError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = LoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
+ ),
+ )
+ }
+ }
}
}
@@ -302,7 +313,7 @@ data class LoginState(
*/
@Parcelize
data class Error(
- val title: Text?,
+ val title: Text? = null,
val message: Text,
) : DialogState()
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 80e7d28d75..17abb29583 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
@@ -254,6 +254,17 @@ class LoginWithDeviceViewModel @Inject constructor(
is LoginResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
}
+
+ LoginResult.CertificateError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = LoginWithDeviceState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
+ ),
+ )
+ }
+ }
}
}
@@ -450,7 +461,7 @@ data class LoginWithDeviceState(
*/
@Parcelize
data class Error(
- val title: Text?,
+ val title: Text? = null,
val message: Text,
) : DialogState()
}
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 71672d79ca..1cb4f8244b 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
@@ -323,6 +323,16 @@ class TwoFactorLoginViewModel @Inject constructor(
// NO-OP: Let the auth flow handle navigation after this.
is LoginResult.Success -> Unit
+ LoginResult.CertificateError -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ title = R.string.an_error_has_occurred.asText(),
+ message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
+ ),
+ )
+ }
+ }
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b56965a1db..8e2dea56a3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1089,5 +1089,5 @@ Do you want to switch to this account?
Done
Biometrics are no longer supported on this device
You’ve been logged out because your device’s biometrics don’t meet the latest security requirements. To update settings, log in once again or contact your administrator for access.
-
+ We couldn’t verify the server’s certificate. The certificate chain or proxy settings on your device or your Bitwarden server may not be set up correctly.
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 72b32e01fc..79449ce499 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
@@ -152,6 +152,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
+import javax.net.ssl.SSLHandshakeException
@Suppress("LargeClass")
class AuthRepositoryTest {
@@ -1510,6 +1511,43 @@ class AuthRepositoryTest {
coVerify { identityService.preLogin(email = EMAIL) }
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `login get token fails should return CertificateError when SSLHandshakeException is thrown`() =
+ 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 SSLHandshakeException("error").asFailure()
+ val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
+ assertEquals(LoginResult.CertificateError, result)
+ assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
+ coVerify { identityService.preLogin(email = EMAIL) }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `prelogin fails should return CertificateError when SSLHandshakeException is thrown`() =
+ runTest {
+ coEvery {
+ identityService.preLogin(email = EMAIL)
+ } returns SSLHandshakeException("error").asFailure()
+ val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
+ assertEquals(LoginResult.CertificateError, 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 {
coEvery {
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtilsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtilsTest.kt
index 9eba24c99c..6c83c277ba 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtilsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtilsTest.kt
@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import java.net.UnknownHostException
+import java.security.cert.CertPathValidatorException
+import javax.net.ssl.SSLHandshakeException
class NetworkUtilsTest {
@Test
@@ -57,4 +59,37 @@ class NetworkUtilsTest {
IllegalStateException().isNoConnectionError(),
)
}
+
+ @Test
+ fun `isSslHandshakeError should return return true for SSLHandshakeException`() {
+ assertEquals(
+ true,
+ SSLHandshakeException("whoops").isSslHandShakeError(),
+ )
+ }
+
+ @Test
+ fun `isSslHandshakeError should return return true for CertPathValidatorException`() {
+ assertEquals(
+ true,
+ CertPathValidatorException("whoops").isSslHandShakeError(),
+ )
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `isSslHandshakeError should return return true if exceptions cause is SSLHandshakeException`() {
+ assertEquals(
+ true,
+ Exception(SSLHandshakeException("whoops")).isSslHandShakeError(),
+ )
+ }
+
+ @Test
+ fun `isSslHandshakeError should return return false for not IllegalStateException`() {
+ assertEquals(
+ false,
+ IllegalStateException().isSslHandShakeError(),
+ )
+ }
}
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 4e8aac0449..fbbf3ed7f9 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
@@ -444,6 +444,73 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ssoCallbackResultFlow Success with same state with login CertificateError should show loading dialog then show certificate error dialog`() =
+ runTest {
+ val orgIdentifier = "Bitwarden"
+ coEvery {
+ authRepository.login(any(), any(), any(), any(), any(), any())
+ } returns LoginResult.CertificateError
+
+ 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.we_couldnt_verify_the_servers_certificate.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 cc06ce5aa2..5dc89ef94a 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
@@ -319,6 +319,44 @@ class LoginViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `LoginButtonClick login returns CertificateError should update errorDialogState`() =
+ runTest {
+ coEvery {
+ authRepository.login(
+ email = EMAIL,
+ password = "",
+ captchaToken = null,
+ )
+ } returns LoginResult.CertificateError
+ 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.we_couldnt_verify_the_servers_certificate.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 8b67e2f609..bcdca5c9ff 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
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
+@Suppress("LargeClass")
class LoginWithDeviceViewModelTest : BaseViewModelTest() {
private val mutableCreateAuthRequestWithUpdatesFlow =
@@ -444,6 +445,73 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on createAuthRequestWithUpdates Success and login CertificateError 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.CertificateError
+ val viewModel = createViewModel()
+ viewModel.eventFlow.test {
+ viewModel.stateFlow.test {
+ assertEquals(DEFAULT_STATE, awaitItem())
+ mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
+ CreateAuthRequestResult.Success(
+ authRequest = AUTH_REQUEST,
+ privateKey = AUTH_REQUEST_PRIVATE_KEY,
+ accessCode = AUTH_REQUEST_ACCESS_CODE,
+ ),
+ )
+ 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.we_couldnt_verify_the_servers_certificate.asText(),
+ ),
+ loginData = DEFAULT_LOGIN_DATA,
+ ),
+ awaitItem(),
+ )
+ }
+ }
+
+ coVerify(exactly = 1) {
+ authRepository.login(
+ email = EMAIL,
+ requestId = AUTH_REQUEST.id,
+ accessCode = AUTH_REQUEST_ACCESS_CODE,
+ asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
+ requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
+ 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 a5063c496d..97272b5fdc 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
@@ -706,6 +706,66 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ContinueButtonClick login returns CertificateError 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.CertificateError
+
+ 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.we_couldnt_verify_the_servers_certificate.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()