Skip to content

Commit a040a38

Browse files
PM-19296: Propagate login errors to the UI (#4885)
1 parent ef3b773 commit a040a38

File tree

15 files changed

+99
-38
lines changed

15 files changed

+99
-38
lines changed

app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt

+19-6
Original file line numberDiff line numberDiff line change
@@ -585,10 +585,13 @@ class AuthRepositoryImpl(
585585
asymmetricalKey: String,
586586
): LoginResult {
587587
val profile = authDiskSource.userState?.activeAccount?.profile
588-
?: return LoginResult.Error(errorMessage = null)
588+
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
589589
val userId = profile.userId
590590
val privateKey = authDiskSource.getPrivateKey(userId = userId)
591-
?: return LoginResult.Error(errorMessage = null)
591+
?: return LoginResult.Error(
592+
errorMessage = null,
593+
error = MissingPropertyException("Private Key"),
594+
)
592595

593596
checkForVaultUnlockError(
594597
onVaultUnlockError = { error ->
@@ -638,7 +641,7 @@ class AuthRepositoryImpl(
638641
onFailure = { throwable ->
639642
when {
640643
throwable.isSslHandShakeError() -> LoginResult.CertificateError
641-
else -> LoginResult.Error(errorMessage = null)
644+
else -> LoginResult.Error(errorMessage = null, error = throwable)
642645
}
643646
},
644647
onSuccess = { it },
@@ -687,7 +690,10 @@ class AuthRepositoryImpl(
687690
orgIdentifier = orgIdentifier,
688691
)
689692
}
690-
?: LoginResult.Error(errorMessage = null)
693+
?: LoginResult.Error(
694+
errorMessage = null,
695+
error = MissingPropertyException("Identity Token Auth Model"),
696+
)
691697

692698
override suspend fun login(
693699
email: String,
@@ -707,7 +713,10 @@ class AuthRepositoryImpl(
707713
orgIdentifier = orgIdentifier,
708714
)
709715
}
710-
?: LoginResult.Error(errorMessage = null)
716+
?: LoginResult.Error(
717+
errorMessage = null,
718+
error = MissingPropertyException("Identity Token Auth Model"),
719+
)
711720

712721
override suspend fun login(
713722
email: String,
@@ -1645,7 +1654,10 @@ class AuthRepositoryImpl(
16451654
LoginResult.UnofficialServerError
16461655
}
16471656

1648-
else -> LoginResult.Error(errorMessage = null)
1657+
else -> LoginResult.Error(
1658+
errorMessage = null,
1659+
error = throwable,
1660+
)
16491661
}
16501662
},
16511663
onSuccess = { loginResponse ->
@@ -1681,6 +1693,7 @@ class AuthRepositoryImpl(
16811693
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
16821694
LoginResult.Error(
16831695
errorMessage = loginResponse.errorMessage,
1696+
error = null,
16841697
)
16851698
}
16861699
}

app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResult.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ sealed class LoginResult {
2222
/**
2323
* There was an error logging in.
2424
*/
25-
data class Error(val errorMessage: String?) : LoginResult()
25+
data class Error(
26+
val errorMessage: String?,
27+
val error: Throwable?,
28+
) : LoginResult()
2629

2730
/**
2831
* There was an error while logging into an unofficial Bitwarden server.

app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensions.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
88
* the necessary `message` if applicable.
99
*/
1010
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
11-
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
11+
is VaultUnlockResult.AuthenticationError -> {
12+
LoginResult.Error(errorMessage = this.message, error = this.error)
13+
}
14+
1215
is VaultUnlockResult.BiometricDecodingError,
1316
is VaultUnlockResult.GenericError,
1417
is VaultUnlockResult.InvalidStateError,
15-
-> LoginResult.Error(errorMessage = null)
18+
-> LoginResult.Error(errorMessage = null, error = this.error)
1619
}

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
161161
showError(
162162
message = loginResult.errorMessage?.asText()
163163
?: R.string.login_sso_error.asText(),
164+
error = loginResult.error,
164165
)
165166
}
166167

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ private fun LoginDialogs(
208208
is LoginState.DialogState.Error -> BitwardenBasicDialog(
209209
title = dialogState.title?.invoke(),
210210
message = dialogState.message(),
211+
throwable = dialogState.error,
211212
onDismissRequest = onDismissRequest,
212213
)
213214

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class LoginViewModel @Inject constructor(
172172
title = R.string.an_error_has_occurred.asText(),
173173
message = loginResult.errorMessage?.asText()
174174
?: R.string.generic_error_message.asText(),
175+
error = loginResult.error,
175176
),
176177
)
177178
}
@@ -326,6 +327,7 @@ data class LoginState(
326327
data class Error(
327328
val title: Text? = null,
328329
val message: Text,
330+
val error: Throwable? = null,
329331
) : DialogState()
330332

331333
/**

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt

+1
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ class LoginWithDeviceViewModel @Inject constructor(
236236
.errorMessage
237237
?.asText()
238238
?: R.string.generic_error_message.asText(),
239+
error = loginResult.error,
239240
),
240241
)
241242
}

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ private fun TwoFactorLoginDialogs(
202202
?.invoke()
203203
?: stringResource(R.string.an_error_has_occurred),
204204
message = dialogState.message(),
205+
throwable = dialogState.error,
205206
onDismissRequest = onDismissRequest,
206207
)
207208

app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt

+2
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ class TwoFactorLoginViewModel @Inject constructor(
308308
title = R.string.an_error_has_occurred.asText(),
309309
message = loginResult.errorMessage?.asText()
310310
?: R.string.invalid_verification_code.asText(),
311+
error = loginResult.error,
311312
),
312313
)
313314
}
@@ -658,6 +659,7 @@ data class TwoFactorLoginState(
658659
data class Error(
659660
val title: Text? = null,
660661
val message: Text,
662+
val error: Throwable? = null,
661663
) : DialogState()
662664

663665
/**

app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt

+45-21
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,10 @@ class AuthRepositoryTest {
13651365
requestPrivateKey = requestPrivateKey,
13661366
asymmetricalKey = asymmetricalKey,
13671367
)
1368-
assertEquals(LoginResult.Error(errorMessage = null), result)
1368+
assertEquals(
1369+
LoginResult.Error(errorMessage = null, error = NoActiveUserException()),
1370+
result,
1371+
)
13691372
}
13701373

13711374
@Test
@@ -1377,7 +1380,10 @@ class AuthRepositoryTest {
13771380
requestPrivateKey = requestPrivateKey,
13781381
asymmetricalKey = asymmetricalKey,
13791382
)
1380-
assertEquals(LoginResult.Error(errorMessage = null), result)
1383+
assertEquals(
1384+
LoginResult.Error(errorMessage = null, error = MissingPropertyException("Private Key")),
1385+
result,
1386+
)
13811387
}
13821388

13831389
@Test
@@ -1474,16 +1480,17 @@ class AuthRepositoryTest {
14741480
vaultRepository.syncIfNecessary()
14751481
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
14761482
}
1477-
assertEquals(LoginResult.Error(errorMessage = null), result)
1483+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
14781484
}
14791485

14801486
@Test
14811487
fun `login when pre login fails should return Error with no message`() = runTest {
1488+
val error = RuntimeException()
14821489
coEvery {
14831490
identityService.preLogin(email = EMAIL)
1484-
} returns RuntimeException().asFailure()
1491+
} returns error.asFailure()
14851492
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1486-
assertEquals(LoginResult.Error(errorMessage = null), result)
1493+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
14871494
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
14881495
coVerify { identityService.preLogin(email = EMAIL) }
14891496
}
@@ -1492,6 +1499,7 @@ class AuthRepositoryTest {
14921499
@Test
14931500
fun `login get token fails should return Error with no message when server is an official Bitwarden server`() =
14941501
runTest {
1502+
val error = RuntimeException()
14951503
coEvery {
14961504
identityService.preLogin(email = EMAIL)
14971505
} returns PRE_LOGIN_SUCCESS.asSuccess()
@@ -1505,9 +1513,9 @@ class AuthRepositoryTest {
15051513
captchaToken = null,
15061514
uniqueAppId = UNIQUE_APP_ID,
15071515
)
1508-
} returns RuntimeException().asFailure()
1516+
} returns error.asFailure()
15091517
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1510-
assertEquals(LoginResult.Error(errorMessage = null), result)
1518+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
15111519
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
15121520
coVerify { identityService.preLogin(email = EMAIL) }
15131521
coVerify {
@@ -1609,7 +1617,7 @@ class AuthRepositoryTest {
16091617
.asSuccess()
16101618

16111619
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1612-
assertEquals(LoginResult.Error(errorMessage = "mock_error_message"), result)
1620+
assertEquals(LoginResult.Error(errorMessage = "mock_error_message", error = null), result)
16131621
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
16141622
coVerify { identityService.preLogin(email = EMAIL) }
16151623
coVerify {
@@ -1790,7 +1798,10 @@ class AuthRepositoryTest {
17901798
)
17911799
} returns SINGLE_USER_STATE_1
17921800
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
1793-
assertEquals(LoginResult.Error(errorMessage = expectedErrorMessage), result)
1801+
assertEquals(
1802+
LoginResult.Error(errorMessage = expectedErrorMessage, error = error),
1803+
result,
1804+
)
17941805
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
17951806
coVerify { identityService.preLogin(email = EMAIL) }
17961807
fakeAuthDiskSource.assertPrivateKey(
@@ -2262,7 +2273,7 @@ class AuthRepositoryTest {
22622273
captchaToken = null,
22632274
orgIdentifier = null,
22642275
)
2265-
assertEquals(LoginResult.Error(errorMessage = null), finalResult)
2276+
assertEquals(LoginResult.Error(errorMessage = null, error = error), finalResult)
22662277
assertEquals(twoFactorResponse, repository.twoFactorResponse)
22672278
fakeAuthDiskSource.assertTwoFactorToken(
22682279
email = EMAIL,
@@ -2374,11 +2385,18 @@ class AuthRepositoryTest {
23742385
captchaToken = null,
23752386
orgIdentifier = null,
23762387
)
2377-
assertEquals(LoginResult.Error(errorMessage = null), result)
2388+
assertEquals(
2389+
LoginResult.Error(
2390+
errorMessage = null,
2391+
error = MissingPropertyException("Identity Token Auth Model"),
2392+
),
2393+
result,
2394+
)
23782395
}
23792396

23802397
@Test
23812398
fun `login with device get token fails should return Error with no message`() = runTest {
2399+
val error = Throwable("Fail!")
23822400
coEvery {
23832401
identityService.getToken(
23842402
email = EMAIL,
@@ -2390,7 +2408,7 @@ class AuthRepositoryTest {
23902408
captchaToken = null,
23912409
uniqueAppId = UNIQUE_APP_ID,
23922410
)
2393-
} returns Throwable("Fail").asFailure()
2411+
} returns error.asFailure()
23942412
val result = repository.login(
23952413
email = EMAIL,
23962414
requestId = DEVICE_REQUEST_ID,
@@ -2400,7 +2418,7 @@ class AuthRepositoryTest {
24002418
masterPasswordHash = PASSWORD_HASH,
24012419
captchaToken = null,
24022420
)
2403-
assertEquals(LoginResult.Error(errorMessage = null), result)
2421+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
24042422
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
24052423
coVerify {
24062424
identityService.getToken(
@@ -2447,7 +2465,10 @@ class AuthRepositoryTest {
24472465
masterPasswordHash = PASSWORD_HASH,
24482466
captchaToken = null,
24492467
)
2450-
assertEquals(LoginResult.Error(errorMessage = "mock_error_message"), result)
2468+
assertEquals(
2469+
LoginResult.Error(errorMessage = "mock_error_message", error = null),
2470+
result,
2471+
)
24512472
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
24522473
coVerify {
24532474
identityService.getToken(
@@ -2849,6 +2870,7 @@ class AuthRepositoryTest {
28492870

28502871
@Test
28512872
fun `SSO login get token fails should return Error with no message`() = runTest {
2873+
val error = RuntimeException()
28522874
coEvery {
28532875
identityService.getToken(
28542876
email = EMAIL,
@@ -2860,7 +2882,7 @@ class AuthRepositoryTest {
28602882
captchaToken = null,
28612883
uniqueAppId = UNIQUE_APP_ID,
28622884
)
2863-
} returns RuntimeException().asFailure()
2885+
} returns error.asFailure()
28642886
val result = repository.login(
28652887
email = EMAIL,
28662888
ssoCode = SSO_CODE,
@@ -2869,7 +2891,7 @@ class AuthRepositoryTest {
28692891
captchaToken = null,
28702892
organizationIdentifier = ORGANIZATION_IDENTIFIER,
28712893
)
2872-
assertEquals(LoginResult.Error(errorMessage = null), result)
2894+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
28732895
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
28742896
coVerify {
28752897
identityService.getToken(
@@ -2914,7 +2936,7 @@ class AuthRepositoryTest {
29142936
captchaToken = null,
29152937
organizationIdentifier = ORGANIZATION_IDENTIFIER,
29162938
)
2917-
assertEquals(LoginResult.Error(errorMessage = "mock_error_message"), result)
2939+
assertEquals(LoginResult.Error(errorMessage = "mock_error_message", error = null), result)
29182940
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
29192941
coVerify {
29202942
identityService.getToken(
@@ -3059,6 +3081,7 @@ class AuthRepositoryTest {
30593081
@Suppress("MaxLineLength")
30603082
fun `SSO login get token succeeds with key connector and no master password should return failure`() =
30613083
runTest {
3084+
val error = Throwable("Fail!")
30623085
val keyConnectorUrl = "www.example.com"
30633086
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
30643087
keyConnectorUrl = keyConnectorUrl,
@@ -3084,7 +3107,7 @@ class AuthRepositoryTest {
30843107
url = keyConnectorUrl,
30853108
accessToken = ACCESS_TOKEN,
30863109
)
3087-
} returns Throwable("Fail").asFailure()
3110+
} returns error.asFailure()
30883111
every {
30893112
successResponse.toUserState(
30903113
previousUserState = null,
@@ -3101,7 +3124,7 @@ class AuthRepositoryTest {
31013124
organizationIdentifier = ORGANIZATION_IDENTIFIER,
31023125
)
31033126

3104-
assertEquals(LoginResult.Error(errorMessage = null), result)
3127+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
31053128
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = null)
31063129
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null)
31073130
coVerify(exactly = 1) {
@@ -3227,6 +3250,7 @@ class AuthRepositoryTest {
32273250
@Suppress("MaxLineLength")
32283251
fun `SSO login get token succeeds with key connector, no master password, no key and no private key should return failure`() =
32293252
runTest {
3253+
val error = Throwable("Fail!")
32303254
val keyConnectorUrl = "www.example.com"
32313255
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
32323256
keyConnectorUrl = keyConnectorUrl,
@@ -3259,7 +3283,7 @@ class AuthRepositoryTest {
32593283
kdfParallelism = PROFILE_1.kdfParallelism,
32603284
organizationIdentifier = ORGANIZATION_IDENTIFIER,
32613285
)
3262-
} returns Throwable("Fail").asFailure()
3286+
} returns error.asFailure()
32633287
every {
32643288
successResponse.toUserState(
32653289
previousUserState = null,
@@ -3276,7 +3300,7 @@ class AuthRepositoryTest {
32763300
organizationIdentifier = ORGANIZATION_IDENTIFIER,
32773301
)
32783302

3279-
assertEquals(LoginResult.Error(errorMessage = null), result)
3303+
assertEquals(LoginResult.Error(errorMessage = null, error = error), result)
32803304
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = null)
32813305
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null)
32823306
coVerify(exactly = 1) {

0 commit comments

Comments
 (0)