diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index c9953a53a..b0b346b8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -398,6 +398,12 @@ class VaultAddEditViewModel @Inject constructor( request: Fido2CredentialRequest, content: VaultAddEditState.ViewState.Content, ) { + + if (fido2CredentialManager.isUserVerified) { + registerFido2CredentialToCipher(request, content.toCipherView()) + return + } + val createOptions = fido2CredentialManager .getPasskeyCreateOptionsOrNull(request.requestJson) ?: run { @@ -514,10 +520,12 @@ class VaultAddEditViewModel @Inject constructor( } private fun handleUserVerificationLockOut() { + fido2CredentialManager.isUserVerified = false showFido2ErrorDialog() } private fun handleUserVerificationSuccess() { + fido2CredentialManager.isUserVerified = true specialCircumstanceManager .specialCircumstance ?.toFido2RequestOrNull() @@ -533,10 +541,12 @@ class VaultAddEditViewModel @Inject constructor( } private fun handleUserVerificationFail() { + fido2CredentialManager.isUserVerified = false showFido2ErrorDialog() } private fun handleFido2ErrorDialogDismissed() { + fido2CredentialManager.isUserVerified = false clearDialogState() sendEvent( VaultAddEditEvent.CompleteFido2Registration( @@ -546,6 +556,7 @@ class VaultAddEditViewModel @Inject constructor( } private fun handleUserVerificationCancelled() { + fido2CredentialManager.isUserVerified = false clearDialogState() sendEvent( VaultAddEditEvent.CompleteFido2Registration( @@ -555,12 +566,8 @@ class VaultAddEditViewModel @Inject constructor( } private fun handleUserVerificationNotSupported() { - clearDialogState() - sendEvent( - VaultAddEditEvent.CompleteFido2Registration( - result = Fido2RegisterCredentialResult.Error, - ), - ) + fido2CredentialManager.isUserVerified = false + showFido2ErrorDialog() } private fun handleAddNewCustomFieldClick( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index de0826c31..acba971a9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -122,7 +122,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP) } returns emptyList() } - private val fido2CredentialManager = mockk() + private val fido2CredentialManager = mockk { + every { isUserVerified } returns false + every { isUserVerified = any() } just runs + } private val vaultRepository: VaultRepository = mockk { every { vaultDataStateFlow } returns mutableVaultDataFlow every { totpCodeFlow } returns totpTestCodeFlow @@ -855,18 +858,64 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `in add mode during fido2, SaveClick should skip user verification when user is verified`() = + runTest { + val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1) + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.Fido2Save( + fido2CredentialRequest = fido2CredentialRequest, + ) + val stateWithName = createVaultAddItemState( + vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + ), + ) + .copy(shouldExitOnSave = true) + + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), + ), + ) + + every { authRepository.activeUserId } returns fido2CredentialRequest.userId + every { fido2CredentialManager.isUserVerified } returns true + coEvery { + fido2CredentialManager.registerFido2Credential( + userId = fido2CredentialRequest.userId, + fido2CredentialRequest = fido2CredentialRequest, + selectedCipherView = any(), + ) + } returns Fido2RegisterCredentialResult.Success(registrationResponse = "mockResponse") + + viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) + + coVerify { + fido2CredentialManager.registerFido2Credential( + userId = fido2CredentialRequest.userId, + fido2CredentialRequest = fido2CredentialRequest, + selectedCipherView = any(), + ) + } + + verify(exactly = 0) { + fido2CredentialManager.getPasskeyCreateOptionsOrNull(any()) + } + } + @Suppress("MaxLineLength") @Test fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() = runTest { val mockUserId = "mockUserId" - val fido2CredentialRequest = Fido2CredentialRequest( - userId = mockUserId, - requestJson = "mockRequestJson", - packageName = "mockPackageName", - signingInfo = mockk(), - origin = null, - ) + val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1) specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save( fido2CredentialRequest = fido2CredentialRequest, @@ -910,14 +959,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() = runTest { - val mockUserId = "mockUserId" - val fido2CredentialRequest = Fido2CredentialRequest( - userId = mockUserId, - requestJson = "mockRequestJson", - packageName = "mockPackageName", - signingInfo = mockk(), - origin = null, - ) + val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1) specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save( fido2CredentialRequest = fido2CredentialRequest, @@ -963,14 +1005,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in add mode during fido2, SaveClick should emit fido user verification as required when request user verification option is REQUIRED`() = runTest { - val mockUserId = "mockUserId" - val fido2CredentialRequest = Fido2CredentialRequest( - userId = mockUserId, - requestJson = "mockRequestJson", - packageName = "mockPackageName", - signingInfo = mockk(), - origin = null, - ) + val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1) specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save( fido2CredentialRequest = fido2CredentialRequest, @@ -1012,6 +1047,51 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `in add mode during fido2, SaveClick should show Fido2ErrorDialog when user is not verified and registration user verification option is null`() = + runTest { + val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1) + val stateWithName = createVaultAddItemState( + vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + ), + ) + .copy(shouldExitOnSave = true) + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.Fido2Save( + fido2CredentialRequest = fido2CredentialRequest, + ) + every { + fido2CredentialManager.getPasskeyCreateOptionsOrNull( + requestJson = fido2CredentialRequest.requestJson, + ) + } returns createMockPublicKeyCredentialCreationOptions( + number = 1, + userVerificationRequirement = null, + ) + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(), + ) + val viewModel = createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), + ), + ) + + viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) + + assertEquals( + VaultAddEditState.DialogState.Fido2Error( + message = R.string.passkey_operation_failed_because_user_could_not_be_verified + .asText(), + ), + viewModel.stateFlow.value.dialog, + ) + } + @Test fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() = runTest { @@ -2869,9 +2949,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `UserVerificationLockout should display Fido2ErrorDialog`() { + fun `UserVerificationLockout should set isUserVerified to false and display Fido2ErrorDialog`() { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut) + verify { fido2CredentialManager.isUserVerified = false } assertEquals( VaultAddEditState.DialogState.Fido2Error( message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(), @@ -2882,10 +2963,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `UserVerificationCancelled should clear dialog state and emit CompleteFido2Create with cancelled result`() = + fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Create with cancelled result`() = runTest { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled) + verify { fido2CredentialManager.isUserVerified = false } assertNull(viewModel.stateFlow.value.dialog) viewModel.eventFlow.test { assertEquals( @@ -2899,9 +2981,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `UserVerificationFail should display Fido2ErrorDialog`() { + fun `UserVerificationFail should set isUserVerified to false, and display Fido2ErrorDialog`() { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail) + verify { fido2CredentialManager.isUserVerified = false } assertEquals( VaultAddEditState.DialogState.Fido2Error( message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(), @@ -2910,6 +2993,17 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `UserVerificationNotSupported should set isUserVerified to false and show Fido2ErrorDialog`() { + viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported) + verify { fido2CredentialManager.isUserVerified = false } + assertEquals( + VaultAddEditState.DialogState.Fido2Error(R.string.passkey_operation_failed_because_user_could_not_be_verified.asText()), + viewModel.stateFlow.value.dialog, + ) + } + @Suppress("MaxLineLength") @Test fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() = @@ -2985,8 +3079,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") @Test - fun `UserVerificationSuccess should register FIDO 2 credential`() = + fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential`() = runTest { val mockRequest = createMockFido2CredentialRequest(number = 1) val mockResult = Fido2RegisterCredentialResult.Success( @@ -3003,10 +3098,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { any(), ) } returns mockResult + every { fido2CredentialManager.isUserVerified } returns true viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess) coVerify { + fido2CredentialManager.isUserVerified = true fido2CredentialManager.registerFido2Credential( userId = any(), fido2CredentialRequest = mockRequest, @@ -3026,23 +3123,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } - @Suppress("MaxLineLength") - @Test - fun `UserVerificationNotSupported should clear dialog state and send CompleteFido2Registration event with Error`() = - runTest { - viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported) - - viewModel.eventFlow.test { - assertNull(viewModel.stateFlow.value.dialog) - assertEquals( - VaultAddEditEvent.CompleteFido2Registration( - result = Fido2RegisterCredentialResult.Error, - ), - awaitItem(), - ) - } - } - @Suppress("MaxLineLength") @Test fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =