diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt index d2e61c3f49..b2768cfb26 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt @@ -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 = 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 = 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 = 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 { val profile = authDiskSource.userState?.activeAccount?.profile - ?: return IllegalStateException("No active account").asFailure() + ?: return NoActiveUserException().asFailure() return authSdkSource.getUserFingerprint( email = profile.email, publicKey = publicKey, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestResult.kt index ddc1947a3b..db41eb94d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestResult.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestUpdatesResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestUpdatesResult.kt index 11dbc535df..4cb5c9f9ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestUpdatesResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestUpdatesResult.kt @@ -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. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsResult.kt index 608d18df89..66aebb0c99 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsResult.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsUpdatesResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsUpdatesResult.kt index 1a5aa2ea98..576320b0b2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsUpdatesResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/AuthRequestsUpdatesResult.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/error/NoActiveUserException.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/error/NoActiveUserException.kt new file mode 100644 index 0000000000..bb19cfb48f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/error/NoActiveUserException.kt @@ -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!") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt index 3f2a85a7e7..a88704ad6f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt @@ -296,6 +296,7 @@ private fun LoginApprovalDialogs( is LoginApprovalState.DialogState.Error -> BitwardenBasicDialog( title = state.title?.invoke(), message = state.message(), + throwable = state.error, onDismissRequest = onDismissError, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt index b09cd9aa0b..e735525303 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt @@ -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() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt index 812b952eb3..2d0950b523 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt @@ -169,7 +169,7 @@ class PendingRequestsViewModel @Inject constructor( } } - AuthRequestsUpdatesResult.Error -> { + is AuthRequestsUpdatesResult.Error -> { mutableStateFlow.update { it.copy( authRequests = emptyList(), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt index 5d682bfc90..74836f2fb0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt @@ -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 diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt index 4d54f8d27c..e4f14de790 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt @@ -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, ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt index 1637f48039..c622032990 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt @@ -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) }