mirror of
https://github.com/bitwarden/android.git
synced 2026-03-21 22:00:42 -05:00
PM-19233: Propagate auth request errors to the UI (#4868)
This commit is contained in:
@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.isSso
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
@@ -51,7 +52,9 @@ class AuthRequestManagerImpl(
|
||||
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
when (val result = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
|
||||
is AuthRequestsResult.Error -> {
|
||||
emit(AuthRequestsUpdatesResult.Error(error = result.error))
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
|
||||
@@ -181,7 +184,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error) },
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error(error = it)) },
|
||||
onSuccess = { updateAuthRequest ->
|
||||
when {
|
||||
updateAuthRequest.requestApproved -> {
|
||||
@@ -217,13 +220,18 @@ class AuthRequestManagerImpl(
|
||||
fingerprint: String,
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
when (val authRequestsResult = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> AuthRequestUpdatesResult.Error
|
||||
is AuthRequestsResult.Error -> {
|
||||
AuthRequestUpdatesResult.Error(error = authRequestsResult.error)
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
authRequestsResult
|
||||
.authRequests
|
||||
.firstOrNull { it.fingerprint == fingerprint }
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
?: AuthRequestUpdatesResult.Error(
|
||||
error = IllegalStateException("Could not find the auth request."),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,30 +241,28 @@ class AuthRequestManagerImpl(
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
authRequestsService
|
||||
.getAuthRequest(requestId)
|
||||
.map { response ->
|
||||
getFingerprintPhrase(response.publicKey).getOrNull()?.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
.mapCatching { response ->
|
||||
getFingerprintPhrase(response.publicKey)
|
||||
.getOrThrow()
|
||||
.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestUpdatesResult.Error },
|
||||
onSuccess = { authRequest ->
|
||||
authRequest
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
},
|
||||
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -308,7 +314,7 @@ class AuthRequestManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestsResult.Error },
|
||||
onFailure = { AuthRequestsResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestsResult.Success(authRequests = it) },
|
||||
)
|
||||
|
||||
@@ -318,7 +324,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult {
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.getAuthRequestKey(
|
||||
publicKey = publicKey,
|
||||
@@ -349,7 +355,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onFailure = { AuthRequestResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestResult.Success(authRequest = it) },
|
||||
)
|
||||
}
|
||||
@@ -461,7 +467,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
): Result<String> {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return IllegalStateException("No active account").asFailure()
|
||||
?: return NoActiveUserException().asFailure()
|
||||
return authSdkSource.getUserFingerprint(
|
||||
email = profile.email,
|
||||
publicKey = publicKey,
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestResult()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ sealed class AuthRequestUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestUpdatesResult()
|
||||
|
||||
/**
|
||||
* The auth request has been declined.
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsUpdatesResult()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that there is currently no active user when one is required.
|
||||
*/
|
||||
class NoActiveUserException : IllegalStateException("No current active user!")
|
||||
@@ -296,6 +296,7 @@ private fun LoginApprovalDialogs(
|
||||
is LoginApprovalState.DialogState.Error -> BitwardenBasicDialog(
|
||||
title = state.title?.invoke(),
|
||||
message = state.message(),
|
||||
throwable = state.error,
|
||||
onDismissRequest = onDismissError,
|
||||
)
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
private fun handleApproveRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.ApproveRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
when (val result = action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.ShowToast(R.string.login_approved.asText()))
|
||||
sendClosingEvent()
|
||||
@@ -189,6 +189,7 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
dialogState = LoginApprovalState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -239,7 +240,7 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
private fun handleDeclineRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.DeclineRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
when (val result = action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.ShowToast(R.string.log_in_denied.asText()))
|
||||
sendClosingEvent()
|
||||
@@ -251,6 +252,7 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
dialogState = LoginApprovalState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -327,6 +329,7 @@ data class LoginApprovalState(
|
||||
data class Error(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
val error: Throwable? = null,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,7 +169,7 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
AuthRequestsUpdatesResult.Error -> {
|
||||
is AuthRequestsUpdatesResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
authRequests = emptyList(),
|
||||
|
||||
@@ -427,12 +427,13 @@ class AuthRequestManagerTest {
|
||||
fun `getAuthRequestByFingerprintFlow should emit failure and cancel flow when getAuthRequests fails`() =
|
||||
runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
coEvery { authRequestsService.getAuthRequests() } returns Throwable("Fail").asFailure()
|
||||
val error = Throwable("Fail")
|
||||
coEvery { authRequestsService.getAuthRequests() } returns error.asFailure()
|
||||
|
||||
repository
|
||||
.getAuthRequestByFingerprintFlow(fingerprint)
|
||||
.test {
|
||||
assertEquals(AuthRequestUpdatesResult.Error, awaitItem())
|
||||
assertEquals(AuthRequestUpdatesResult.Error(error = error), awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
|
||||
@@ -449,8 +450,9 @@ class AuthRequestManagerTest {
|
||||
authRequests = listOf(AUTH_REQUESTS_RESPONSE_JSON_AUTH_RESPONSE),
|
||||
)
|
||||
val authRequest = AUTH_REQUEST
|
||||
val error = Throwable("Fail")
|
||||
val expectedOne = AuthRequestUpdatesResult.Update(authRequest = authRequest)
|
||||
val expectedTwo = AuthRequestUpdatesResult.Error
|
||||
val expectedTwo = AuthRequestUpdatesResult.Error(error = error)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(email = EMAIL, publicKey = PUBLIC_KEY)
|
||||
} returns FINGER_PRINT.asSuccess()
|
||||
@@ -459,7 +461,7 @@ class AuthRequestManagerTest {
|
||||
} returns authRequestsResponseJson.asSuccess()
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequest(requestId = REQUEST_ID)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
} returns error.asFailure()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE
|
||||
|
||||
repository
|
||||
@@ -639,14 +641,13 @@ class AuthRequestManagerTest {
|
||||
@Test
|
||||
fun `getAuthRequestByIdFlow should emit failure and cancel flow when getAuthRequests fails`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequest(REQUEST_ID)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
val error = Throwable("Fail")
|
||||
coEvery { authRequestsService.getAuthRequest(REQUEST_ID) } returns error.asFailure()
|
||||
|
||||
repository
|
||||
.getAuthRequestByIdFlow(REQUEST_ID)
|
||||
.test {
|
||||
assertEquals(AuthRequestUpdatesResult.Error, awaitItem())
|
||||
assertEquals(AuthRequestUpdatesResult.Error(error = error), awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
|
||||
@@ -660,10 +661,11 @@ class AuthRequestManagerTest {
|
||||
fun `getAuthRequestByIdFlow should emit update then not cancel on failure when initial request succeeds and second fails`() =
|
||||
runTest {
|
||||
val authRequestResponseOne = AUTH_REQUESTS_RESPONSE_JSON_AUTH_RESPONSE.asSuccess()
|
||||
val authRequestResponseTwo = Throwable("Fail").asFailure()
|
||||
val error = Throwable("Fail")
|
||||
val authRequestResponseTwo = error.asFailure()
|
||||
val authRequest = AUTH_REQUEST.copy(id = REQUEST_ID)
|
||||
val expectedOne = AuthRequestUpdatesResult.Update(authRequest = authRequest)
|
||||
val expectedTwo = AuthRequestUpdatesResult.Error
|
||||
val expectedTwo = AuthRequestUpdatesResult.Error(error = error)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(email = EMAIL, publicKey = PUBLIC_KEY)
|
||||
} returns FINGER_PRINT.asSuccess()
|
||||
@@ -850,11 +852,12 @@ class AuthRequestManagerTest {
|
||||
val authRequestsResponseJson = AuthRequestsResponseJson(
|
||||
authRequests = listOf(AUTH_REQUESTS_RESPONSE_JSON_AUTH_RESPONSE),
|
||||
)
|
||||
val expectedOne = AuthRequestsUpdatesResult.Error
|
||||
val error = Throwable("Fail")
|
||||
val expectedOne = AuthRequestsUpdatesResult.Error(error = error)
|
||||
val expectedTwo = AuthRequestsUpdatesResult.Update(authRequests = authRequests)
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequests()
|
||||
} returns Throwable("Fail").asFailure() andThen authRequestsResponseJson.asSuccess()
|
||||
} returns error.asFailure() andThen authRequestsResponseJson.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(email = EMAIL, publicKey = PUBLIC_KEY)
|
||||
} returns FINGER_PRINT.asSuccess()
|
||||
@@ -935,14 +938,15 @@ class AuthRequestManagerTest {
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests should return failure when service returns failure`() = runTest {
|
||||
coEvery { authRequestsService.getAuthRequests() } returns Throwable("Fail").asFailure()
|
||||
val error = Throwable("Fail")
|
||||
coEvery { authRequestsService.getAuthRequests() } returns error.asFailure()
|
||||
|
||||
val result = repository.getAuthRequests()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRequestsService.getAuthRequests()
|
||||
}
|
||||
assertEquals(AuthRequestsResult.Error, result)
|
||||
assertEquals(AuthRequestsResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1027,9 +1031,10 @@ class AuthRequestManagerTest {
|
||||
|
||||
@Test
|
||||
fun `updateAuthRequest should return failure when sdk returns failure`() = runTest {
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultSdkSource.getAuthRequestKey(publicKey = PUBLIC_KEY, userId = USER_ID)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
} returns error.asFailure()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE
|
||||
|
||||
val result = repository.updateAuthRequest(
|
||||
@@ -1042,7 +1047,7 @@ class AuthRequestManagerTest {
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getAuthRequestKey(publicKey = PUBLIC_KEY, userId = USER_ID)
|
||||
}
|
||||
assertEquals(AuthRequestResult.Error, result)
|
||||
assertEquals(AuthRequestResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1050,6 +1055,7 @@ class AuthRequestManagerTest {
|
||||
val requestId = "requestId"
|
||||
val passwordHash = "masterPasswordHash"
|
||||
val encodedKey = "encodedKey"
|
||||
val error = Throwable("Mission failed")
|
||||
coEvery {
|
||||
vaultSdkSource.getAuthRequestKey(publicKey = PUBLIC_KEY, userId = USER_ID)
|
||||
} returns encodedKey.asSuccess()
|
||||
@@ -1061,7 +1067,7 @@ class AuthRequestManagerTest {
|
||||
deviceId = UNIQUE_APP_ID,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns Throwable("Mission failed").asFailure()
|
||||
} returns error.asFailure()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE
|
||||
|
||||
val result = repository.updateAuthRequest(
|
||||
@@ -1074,7 +1080,7 @@ class AuthRequestManagerTest {
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getAuthRequestKey(publicKey = PUBLIC_KEY, userId = USER_ID)
|
||||
}
|
||||
assertEquals(AuthRequestResult.Error, result)
|
||||
assertEquals(AuthRequestResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -210,7 +210,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
||||
viewState = LoginApprovalState.ViewState.Error,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
mutableAuthRequestSharedFlow.tryEmit(AuthRequestUpdatesResult.Error)
|
||||
mutableAuthRequestSharedFlow.tryEmit(AuthRequestUpdatesResult.Error(error = Throwable()))
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@@ -383,6 +383,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `on ErrorDialogDismiss should update state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val error = Throwable("Fail!")
|
||||
coEvery {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
@@ -390,7 +391,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns AuthRequestResult.Error
|
||||
} returns AuthRequestResult.Error(error = error)
|
||||
viewModel.trySendAction(LoginApprovalAction.DeclineRequestClick)
|
||||
|
||||
assertEquals(
|
||||
@@ -399,6 +400,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
||||
dialogState = LoginApprovalState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
error = error,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -152,7 +152,9 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
||||
viewState = PendingRequestsState.ViewState.Error,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
mutableAuthRequestsWithUpdatesFlow.tryEmit(AuthRequestsUpdatesResult.Error)
|
||||
mutableAuthRequestsWithUpdatesFlow.tryEmit(
|
||||
value = AuthRequestsUpdatesResult.Error(error = Throwable()),
|
||||
)
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user