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()