From 4c983525d3feff779c532f1faa5ab412b58d8a38 Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:27:58 -0400 Subject: [PATCH] PM-11310 handle email registration special circumstance after successful login (#3831) --- .../java/com/x8bit/bitwarden/MainViewModel.kt | 2 +- .../manager/SpecialCircumstanceManagerImpl.kt | 25 +++++++- .../di/ActivityPlatformManagerModule.kt | 12 +++- .../manager/model/SpecialCircumstance.kt | 29 ++++++--- .../util/SpecialCircumstanceExtensions.kt | 4 +- .../CompleteRegistrationScreen.kt | 5 ++ .../CompleteRegistrationViewModel.kt | 10 +--- .../ui/auth/feature/login/LoginViewModel.kt | 6 +- .../feature/rootnav/RootNavViewModel.kt | 4 +- .../com/x8bit/bitwarden/MainViewModelTest.kt | 11 +++- .../manager/SpecialCircumstanceManagerTest.kt | 59 ++++++++++++++++++- .../CompleteRegistrationScreenTest.kt | 8 ++- .../CompleteRegistrationViewModelTest.kt | 49 +++++++-------- .../ui/platform/base/BaseComposeTest.kt | 25 ++++++++ .../feature/rootnav/RootNavViewModelTest.kt | 16 +++-- .../feature/search/SearchViewModelTest.kt | 8 ++- .../loginapproval/LoginApprovalScreenTest.kt | 10 +++- .../feature/send/addsend/AddSendScreenTest.kt | 8 ++- .../addedit/VaultAddEditViewModelTest.kt | 8 ++- .../VaultItemListingViewModelTest.kt | 10 +++- 20 files changed, 240 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index 26e4254cc..d3816a28f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -210,7 +210,7 @@ class MainViewModel @Inject constructor( authRepository.hasPendingAccountAddition = true } specialCircumstanceManager.specialCircumstance = - SpecialCircumstance.CompleteRegistration( + SpecialCircumstance.PreLogin.CompleteRegistration( completeRegistrationData = completeRegistrationData, timestamp = clock.millis(), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerImpl.kt index d63c177f8..c35b46feb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerImpl.kt @@ -1,14 +1,37 @@ package com.x8bit.bitwarden.data.platform.manager +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach /** * Primary implementation of [SpecialCircumstanceManager]. */ -class SpecialCircumstanceManagerImpl : SpecialCircumstanceManager { +class SpecialCircumstanceManagerImpl( + authRepository: AuthRepository, + dispatcherManager: DispatcherManager, +) : SpecialCircumstanceManager { private val mutableSpecialCircumstanceFlow = MutableStateFlow(null) + private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + init { + authRepository + .userStateFlow + .filter { + it?.activeAccount?.isLoggedIn == true + } + .onEach { _ -> + if (specialCircumstance is SpecialCircumstance.PreLogin) { + specialCircumstance = null + } + } + .launchIn(unconfinedScope) + } override var specialCircumstance: SpecialCircumstance? get() = mutableSpecialCircumstanceFlow.value diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/ActivityPlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/ActivityPlatformManagerModule.kt index 09a274ad9..14be661f8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/ActivityPlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/ActivityPlatformManagerModule.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.data.platform.manager.di import com.x8bit.bitwarden.MainActivity +import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,6 +22,12 @@ class ActivityPlatformManagerModule { @Provides @ActivityRetainedScoped - fun provideActivityScopedSpecialCircumstanceRepository(): SpecialCircumstanceManager = - SpecialCircumstanceManagerImpl() + fun provideActivityScopedSpecialCircumstanceRepository( + authRepository: AuthRepository, + dispatcher: DispatcherManager, + ): SpecialCircumstanceManager = + SpecialCircumstanceManagerImpl( + authRepository = authRepository, + dispatcherManager = dispatcher, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt index 915c58b19..24a4a60a3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import kotlinx.parcelize.Parcelize +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager /** * Represents a special circumstance the app may be in. These circumstances could require some kind @@ -50,15 +51,6 @@ sealed class SpecialCircumstance : Parcelable { val shouldFinishWhenComplete: Boolean, ) : SpecialCircumstance() - /** - * The app was launched via AppLink in order to allow the user complete an ongoing registration. - */ - @Parcelize - data class CompleteRegistration( - val completeRegistrationData: CompleteRegistrationData, - val timestamp: Long, - ) : SpecialCircumstance() - /** * The app was launched via the credential manager framework in order to allow the user to * manually save a passkey to their vault. @@ -97,4 +89,23 @@ sealed class SpecialCircumstance : Parcelable { */ @Parcelize data object VaultShortcut : SpecialCircumstance() + + /** + * A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be + * cleared after a successful login. + * + * @see [SpecialCircumstanceManager.clearSpecialCircumstanceAfterLogin] + */ + @Parcelize + sealed class PreLogin : SpecialCircumstance() { + /** + * The app was launched via AppLink in order to allow the user complete an ongoing + * registration. + */ + @Parcelize + data class CompleteRegistration( + val completeRegistrationData: CompleteRegistrationData, + val timestamp: Long, + ) : PreLogin() + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt index ccaaacd93..befdf9770 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt @@ -19,9 +19,9 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? = SpecialCircumstance.GeneratorShortcut -> null SpecialCircumstance.VaultShortcut -> null is SpecialCircumstance.Fido2Save -> null - is SpecialCircumstance.CompleteRegistration -> null is SpecialCircumstance.Fido2Assertion -> null is SpecialCircumstance.Fido2GetCredentials -> null + is SpecialCircumstance.PreLogin.CompleteRegistration -> null } /** @@ -36,9 +36,9 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData? SpecialCircumstance.GeneratorShortcut -> null SpecialCircumstance.VaultShortcut -> null is SpecialCircumstance.Fido2Save -> null - is SpecialCircumstance.CompleteRegistration -> null is SpecialCircumstance.Fido2Assertion -> null is SpecialCircumstance.Fido2GetCredentials -> null + is SpecialCircumstance.PreLogin.CompleteRegistration -> null } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index f6723da67..a824e5ace 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration import android.content.res.Configuration import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -78,6 +79,10 @@ fun CompleteRegistrationScreen( val state by viewModel.stateFlow.collectAsStateWithLifecycle() val handler = rememberCompleteRegistrationHandler(viewModel = viewModel) val context = LocalContext.current + + // route OS back actions through the VM to clear the special circumstance + BackHandler(onBack = handler.onBackClick) + EventsEffect(viewModel) { event -> when (event) { is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index 3d83d4f25..bd04ac399 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject private const val KEY_STATE = "state" @@ -105,13 +104,6 @@ class CompleteRegistrationViewModel @Inject constructor( .launchIn(viewModelScope) } - @VisibleForTesting - public override fun onCleared() { - // clean the specialCircumstance after being handled - specialCircumstanceManager.specialCircumstance = null - super.onCleared() - } - override fun handleAction(action: CompleteRegistrationAction) { when (action) { is ConfirmPasswordInputChange -> handleConfirmPasswordInputChanged(action) @@ -280,6 +272,8 @@ class CompleteRegistrationViewModel @Inject constructor( } private fun handleBackClicked() { + // clear the special circumstance manager as user has elected not to proceed. + specialCircumstanceManager.specialCircumstance = null sendEvent(CompleteRegistrationEvent.NavigateBack) } 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 180c4d777..8218ce66f 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 @@ -35,10 +35,10 @@ private const val KEY_STATE = "state" */ @HiltViewModel class LoginViewModel @Inject constructor( - private val authRepository: AuthRepository, - environmentRepository: EnvironmentRepository, - private val vaultRepository: VaultRepository, savedStateHandle: SavedStateHandle, + environmentRepository: EnvironmentRepository, + private val authRepository: AuthRepository, + private val vaultRepository: VaultRepository, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 85e1f7929..2fd8a02c6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -70,7 +70,7 @@ class RootNavViewModel @Inject constructor( userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword - specialCircumstance is SpecialCircumstance.CompleteRegistration -> { + specialCircumstance is SpecialCircumstance.PreLogin.CompleteRegistration -> { RootNavState.CompleteOngoingRegistration( email = specialCircumstance.completeRegistrationData.email, verificationToken = specialCircumstance.completeRegistrationData.verificationToken, @@ -141,7 +141,7 @@ class RootNavViewModel @Inject constructor( null, -> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId) - is SpecialCircumstance.CompleteRegistration -> { + is SpecialCircumstance.PreLogin.CompleteRegistration -> { throw IllegalStateException( "Special circumstance should have been already handled.", ) diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 629235dc6..638070943 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -27,6 +27,8 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData @@ -86,7 +88,12 @@ class MainViewModelTest : BaseViewModelTest() { private val garbageCollectionManager = mockk { every { tryCollect() } just runs } - private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() + private val mockAuthRepository = mockk(relaxed = true) + private val specialCircumstanceManager: SpecialCircumstanceManager = + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) private val intentManager: IntentManager = mockk { every { getShareDataFromIntent(any()) } returns null } @@ -338,7 +345,7 @@ class MainViewModelTest : BaseViewModelTest() { ), ) assertEquals( - SpecialCircumstance.CompleteRegistration( + SpecialCircumstance.PreLogin.CompleteRegistration( completeRegistrationData = completeRegistrationData, timestamp = FIXED_CLOCK.millis(), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerTest.kt index 78effe3a1..31c71858e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManagerTest.kt @@ -1,16 +1,29 @@ package com.x8bit.bitwarden.data.platform.manager import app.cash.turbine.test +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test class SpecialCircumstanceManagerTest { + private val mutableUserStateFlow = MutableStateFlow(null) + private val mockAuthRepository = mockk(relaxed = true) { + every { userStateFlow } returns mutableUserStateFlow + } + private val specialCircumstanceManager: SpecialCircumstanceManager = - SpecialCircumstanceManagerImpl() + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) @Test fun `specialCircumstanceStateFlow should emit whenever the SpecialCircumstance is updated`() = @@ -29,4 +42,48 @@ class SpecialCircumstanceManagerTest { assertEquals(specialCircumstance2, awaitItem()) } } + + @Suppress("MaxLineLength") + @Test + fun `clearSpecialCircumstanceAfterLogin should clear the SpecialCircumstance if it is a PreLogin`() = + runTest { + specialCircumstanceManager.specialCircumstanceStateFlow.test { + assertNull(awaitItem()) + + val preLoginSpecialCircumstance = + mockk() + + specialCircumstanceManager.specialCircumstance = preLoginSpecialCircumstance + assertEquals(preLoginSpecialCircumstance, awaitItem()) + val mockUserAccount = mockk() { + every { isLoggedIn } returns true + } + val mockUserState = mockk { + every { activeAccount } returns mockUserAccount + } + mutableUserStateFlow.value = mockUserState + + assertNull(awaitItem()) + } + } + + @Suppress("MaxLineLength") + @Test + fun `clearSpecialCircumstanceAfterLogin should not clear the SpecialCircumstance if it is not a PreLogin`() = + runTest { + specialCircumstanceManager.specialCircumstanceStateFlow.test { + assertNull(awaitItem()) + + specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut + assertEquals(SpecialCircumstance.VaultShortcut, awaitItem()) + val mockUserAccount = mockk() { + every { isLoggedIn } returns true + } + val mockUserState = mockk { + every { activeAccount } returns mockUserAccount + } + mutableUserStateFlow.value = mockUserState + expectNoEvents() + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt index 301f533a2..12adc2159 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt @@ -56,7 +56,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() { @Before fun setup() { - composeTestRule.setContent { + setContentWithBackDispatcher { CompleteRegistrationScreen( onNavigateBack = { onNavigateBackCalled = true }, onNavigateToPasswordGuidance = { onNavigateToPasswordGuidanceCalled = true }, @@ -120,6 +120,12 @@ class CompleteRegistrationScreenTest : BaseComposeTest() { assertTrue(onNavigateBackCalled) } + @Test + fun `system back event should send BackClick action`() { + backDispatcher?.onBackPressed() + verify { viewModel.trySendAction(BackClick) } + } + @Test fun `password input change should send PasswordInputChange action`() { composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index 6996b3cff..b35cc707d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -12,13 +12,14 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl -import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData import com.x8bit.bitwarden.data.platform.manager.model.FlagKey -import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.PreLogin import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository @@ -42,6 +43,7 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -53,24 +55,32 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { * Saved state handle that has valid inputs. Useful for tests that want to test things * after the user has entered all valid inputs. */ - private val mockAuthRepository = mockk() + private val mutableUserStateFlow = MutableStateFlow(null) + private val mockAuthRepository = mockk() { + every { userStateFlow } returns mutableUserStateFlow + } private val fakeEnvironmentRepository = FakeEnvironmentRepository() private val specialCircumstanceManager: SpecialCircumstanceManager = - SpecialCircumstanceManagerImpl() + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) private val mutableFeatureFlagFlow = MutableStateFlow(false) private val featureFlagManager = mockk(relaxed = true) { every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow } private val mutableGeneratorResultFlow = bufferedMutableSharedFlow() + private val mockCompleteRegistrationCircumstance = mockk() private val generatorRepository = mockk(relaxed = true) { every { generatorResultFlow } returns mutableGeneratorResultFlow } @BeforeEach fun setUp() { + specialCircumstanceManager.specialCircumstance = mockCompleteRegistrationCircumstance mockkStatic(::generateUriForCaptcha) } @@ -85,29 +95,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) } - @Test - fun `onCleared should erase specialCircumstance`() = runTest { - specialCircumstanceManager.specialCircumstance = SpecialCircumstance.CompleteRegistration( - completeRegistrationData = CompleteRegistrationData( - email = EMAIL, - verificationToken = TOKEN, - fromEmail = true, - ), - System.currentTimeMillis(), - ) - - val viewModel = CompleteRegistrationViewModel( - savedStateHandle = SavedStateHandle(mapOf("state" to DEFAULT_STATE)), - authRepository = mockAuthRepository, - environmentRepository = fakeEnvironmentRepository, - specialCircumstanceManager = specialCircumstanceManager, - featureFlagManager = featureFlagManager, - generatorRepository = generatorRepository, - ) - viewModel.onCleared() - assertTrue(specialCircumstanceManager.specialCircumstance == null) - } - @Test fun `Password below 12 chars should have non-valid state`() = runTest { val input = "abcdefghikl" @@ -371,12 +358,18 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { } @Test - fun `CloseClick should emit NavigateBack`() = runTest { + fun `CloseClick should emit NavigateBack and clear special circumstances`() = runTest { + assertEquals( + mockCompleteRegistrationCircumstance, + specialCircumstanceManager.specialCircumstance, + ) val viewModel = createCompleteRegistrationViewModel() viewModel.eventFlow.test { viewModel.trySendAction(BackClick) assertEquals(CompleteRegistrationEvent.NavigateBack, awaitItem()) } + + assertNull(specialCircumstanceManager.specialCircumstance) } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt index 16f094e9d..d21237d0a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt @@ -1,5 +1,8 @@ package com.x8bit.bitwarden.ui.platform.base +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.createComposeRule import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme @@ -14,6 +17,14 @@ abstract class BaseComposeTest : BaseRobolectricTest() { @get:Rule val composeTestRule = createComposeRule() + /** + * instance of [OnBackPressedDispatcher] made available if testing using + * + * [setContentWithBackDispatcher] or [runTestWithTheme] + */ + var backDispatcher: OnBackPressedDispatcher? = null + private set + /** * Helper for testing a basic Composable function that only requires a Composable environment * with the [BitwardenTheme]. @@ -26,8 +37,22 @@ abstract class BaseComposeTest : BaseRobolectricTest() { BitwardenTheme( theme = theme, ) { + backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher test() } } } + + /** + * Helper for testing a basic Composable function that provides access to a + * [OnBackPressedDispatcher]. + * + * Use if the [Composable] function being tested uses a [BackHandler] + */ + protected fun setContentWithBackDispatcher(test: @Composable () -> Unit) { + composeTestRule.setContent { + backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + test() + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 87f6b41d9..035f773a7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAs import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance @@ -40,7 +42,13 @@ class RootNavViewModelTest : BaseViewModelTest() { every { authStateFlow } returns mutableAuthStateFlow every { showWelcomeCarousel } returns false } - private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() + + private val mockAuthRepository = mockk(relaxed = true) + private val specialCircumstanceManager: SpecialCircumstanceManager = + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) @BeforeEach fun setup() { @@ -655,7 +663,7 @@ class RootNavViewModelTest : BaseViewModelTest() { every { authRepository.hasPendingAccountAddition } returns false specialCircumstanceManager.specialCircumstance = - SpecialCircumstance.CompleteRegistration( + SpecialCircumstance.PreLogin.CompleteRegistration( CompleteRegistrationData( email = "example@email.com", verificationToken = "token", @@ -682,7 +690,7 @@ class RootNavViewModelTest : BaseViewModelTest() { every { authRepository.hasPendingAccountAddition } returns true specialCircumstanceManager.specialCircumstance = - SpecialCircumstance.CompleteRegistration( + SpecialCircumstance.PreLogin.CompleteRegistration( CompleteRegistrationData( email = "example@email.com", verificationToken = "token", @@ -732,7 +740,7 @@ class RootNavViewModelTest : BaseViewModelTest() { every { authRepository.hasPendingAccountAddition } returns true specialCircumstanceManager.specialCircumstance = - SpecialCircumstance.CompleteRegistration( + SpecialCircumstance.PreLogin.CompleteRegistration( CompleteRegistrationData( email = "example@email.com", verificationToken = "token", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index a2c102c87..9cec3c0f3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl @@ -107,8 +108,13 @@ class SearchViewModelTest : BaseViewModelTest() { every { isIconLoadingDisabled } returns false every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow } + + private val mockAuthRepository = mockk(relaxed = true) private val specialCircumstanceManager: SpecialCircumstanceManager = - SpecialCircumstanceManagerImpl() + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) private val organizationEventManager = mockk { every { trackEvent(event = any()) } just runs } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreenTest.kt index 918219b6d..fd966cb85 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreenTest.kt @@ -37,7 +37,7 @@ class LoginApprovalScreenTest : BaseComposeTest() { @Before fun setUp() { - composeTestRule.setContent { + setContentWithBackDispatcher { LoginApprovalScreen( onNavigateBack = { onNavigateBackCalled = true }, viewModel = viewModel, @@ -52,6 +52,14 @@ class LoginApprovalScreenTest : BaseComposeTest() { assertTrue(onNavigateBackCalled) } + @Test + fun `system back should send CloseClick`() { + backDispatcher?.onBackPressed() + verify { + viewModel.trySendAction(LoginApprovalAction.CloseClick) + } + } + @Test fun `on ExitApp should call exit appliction`() { mutableEventFlow.tryEmit(LoginApprovalEvent.ExitApp) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 8131d740a..46d371ba6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -62,7 +62,7 @@ class AddSendScreenTest : BaseComposeTest() { @Before fun setUp() { - composeTestRule.setContent { + setContentWithBackDispatcher { AddSendScreen( viewModel = viewModel, exitManager = exitManager, @@ -104,6 +104,12 @@ class AddSendScreenTest : BaseComposeTest() { verify { viewModel.trySendAction(AddSendAction.CloseClick) } } + @Test + fun `on system back should send CloseClick`() { + backDispatcher?.onBackPressed() + verify { viewModel.trySendAction(AddSendAction.CloseClick) } + } + @Test fun `display navigation icon according to state`() { mutableStateFlow.update { it.copy(isShared = false) } 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 6deda3511..05cf531ae 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 @@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl @@ -137,8 +138,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { every { vaultDataStateFlow } returns mutableVaultDataFlow every { totpCodeFlow } returns totpTestCodeFlow } + + private val mockAuthRepository = mockk(relaxed = true) private val specialCircumstanceManager: SpecialCircumstanceManager = - SpecialCircumstanceManagerImpl() + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) private val generatorRepository: GeneratorRepository = FakeGeneratorRepository() private val organizationEventManager = mockk { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 6e34418e4..d0048a20a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -26,7 +26,9 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager @@ -143,7 +145,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow every { isUnlockWithPinEnabled } returns false } - private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() + + private val mockAuthRepository = mockk(relaxed = true) + private val specialCircumstanceManager: SpecialCircumstanceManager = + SpecialCircumstanceManagerImpl( + authRepository = mockAuthRepository, + dispatcherManager = FakeDispatcherManager(), + ) private val policyManager: PolicyManager = mockk { every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList() every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()