From 23737ef18e0fad62b40f2fa90fb8b782fa1aab32 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Tue, 6 Jan 2026 12:03:43 +0000 Subject: [PATCH] refactored personal vault migration logic into VaultMigrationManager --- .../vault/manager/VaultMigrationManager.kt | 34 ++ .../manager/VaultMigrationManagerImpl.kt | 67 +++ .../data/vault/manager/VaultSyncManager.kt | 7 - .../vault/manager/VaultSyncManagerImpl.kt | 59 +-- .../vault/manager/di/VaultManagerModule.kt | 24 +- .../feature/rootnav/RootNavViewModel.kt | 6 +- .../manager/VaultMigrationManagerTest.kt | 424 ++++++++++++++++++ .../vault/manager/VaultSyncManagerTest.kt | 407 +---------------- .../feature/rootnav/RootNavViewModelTest.kt | 20 +- 9 files changed, 566 insertions(+), 482 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt new file mode 100644 index 0000000000..0be6d950ce --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt @@ -0,0 +1,34 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.network.model.SyncResponseJson +import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData +import kotlinx.coroutines.flow.StateFlow + +/** + * Manages the migration of personal vault items to organization collections. + * This interface provides a way to check if migration is needed and track migration state. + */ +interface VaultMigrationManager { + /** + * Flow that emits when conditions are met for the user to migrate their personal vault. + * Updated after each sync to reflect current policy and vault state. + */ + val vaultMigrationDataStateFlow: StateFlow + + /** + * Verifies if the user should migrate their personal vault to organization collections + * based on active policies, feature flags, and the provided cipher list. + * + * @param cipherList List of ciphers from the sync response to check for personal items. + */ + fun verifyAndUpdateMigrationState(cipherList: List) + + /** + * Checks if the user should migrate their vault based on policies, feature flags, + * network connectivity, and whether they have personal items. + * + * @param hasPersonalItems Callback to check if the user has personal items. + * @return true if migration conditions are met, false otherwise. + */ + fun shouldMigrateVault(hasPersonalItems: () -> Boolean): Boolean +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt new file mode 100644 index 0000000000..8dba6c383d --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt @@ -0,0 +1,67 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.data.manager.model.FlagKey +import com.bitwarden.network.model.PolicyTypeJson +import com.bitwarden.network.model.SyncResponseJson +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager +import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager +import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * Default implementation of [VaultMigrationManager]. + */ +class VaultMigrationManagerImpl( + private val authDiskSource: AuthDiskSource, + private val policyManager: PolicyManager, + private val featureFlagManager: FeatureFlagManager, + private val connectionManager: NetworkConnectionManager, +) : VaultMigrationManager { + + private val activeUserId: String? get() = authDiskSource.userState?.activeUserId + + private val mutableVaultMigrationDataStateFlow = + MutableStateFlow(value = VaultMigrationData.NoMigrationRequired) + + override val vaultMigrationDataStateFlow: StateFlow + get() = mutableVaultMigrationDataStateFlow.asStateFlow() + + override fun verifyAndUpdateMigrationState(cipherList: List) { + val userId = activeUserId ?: return + + mutableVaultMigrationDataStateFlow.update { + if (shouldMigrateVault { cipherList.any { it.organizationId == null } }) { + val orgId = policyManager.getPersonalOwnershipPolicyOrganizationId() + val orgName = authDiskSource + .getOrganizations(userId = userId) + ?.firstOrNull { it.id == orgId } + ?.name + + if (orgId != null && orgName != null) { + VaultMigrationData.MigrationRequired( + organizationId = orgId, + organizationName = orgName, + ) + } else { + VaultMigrationData.NoMigrationRequired + } + } else { + VaultMigrationData.NoMigrationRequired + } + } + } + + override fun shouldMigrateVault(hasPersonalItems: () -> Boolean): Boolean { + return policyManager + .getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + .any() && + featureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) && + connectionManager.isNetworkConnected && + hasPersonalItems() + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManager.kt index 67e04b8ae9..ff06391472 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManager.kt @@ -5,7 +5,6 @@ import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.vault.DecryptCipherListResult import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult -import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -63,12 +62,6 @@ interface VaultSyncManager { */ val sendDataStateFlow: StateFlow> - /** - * Flow that emits when conditions are met for the user to migrate their personal vault. - * Updated after each sync to reflect current policy and vault state. - */ - val shouldMigratePersonalVaultFlow: StateFlow - /** * Sync the vault data for the current user. * diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt index 41d4650596..0a6ba37fd7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt @@ -3,14 +3,11 @@ package com.x8bit.bitwarden.data.vault.manager import com.bitwarden.collections.CollectionView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.data.manager.dispatcher.DispatcherManager -import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.combineDataStates import com.bitwarden.core.data.repository.util.map import com.bitwarden.core.data.repository.util.updateToPendingOrLoading -import com.bitwarden.network.model.PolicyTypeJson import com.bitwarden.network.model.SyncResponseJson -import com.bitwarden.network.model.SyncResponseJson.Cipher import com.bitwarden.network.service.SyncService import com.bitwarden.network.util.isNoConnectionError import com.bitwarden.vault.DecryptCipherListResult @@ -25,16 +22,12 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.error.NoActiveUserException import com.x8bit.bitwarden.data.platform.error.SecurityStampMismatchException import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager -import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PushManager -import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult -import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -88,9 +81,7 @@ class VaultSyncManagerImpl( private val userLogoutManager: UserLogoutManager, private val userStateManager: UserStateManager, private val vaultLockManager: VaultLockManager, - private val policyManager: PolicyManager, - private val featureFlagManager: FeatureFlagManager, - private val connectionManager: NetworkConnectionManager, + private val vaultMigrationManager: VaultMigrationManager, private val clock: Clock, databaseSchemeManager: DatabaseSchemeManager, pushManager: PushManager, @@ -117,9 +108,6 @@ class VaultSyncManagerImpl( private val mutableDomainsStateFlow = MutableStateFlow>(DataState.Loading) - private val mutableShouldMigratePersonalVaultFlow = - MutableStateFlow(value = VaultMigrationData.NoMigrationRequired) - override val decryptCipherListResultStateFlow: StateFlow> get() = mutableDecryptCipherListResultFlow.asStateFlow() @@ -135,9 +123,6 @@ class VaultSyncManagerImpl( override val sendDataStateFlow: StateFlow> get() = mutableSendDataStateFlow.asStateFlow() - override val shouldMigratePersonalVaultFlow: StateFlow - get() = mutableShouldMigratePersonalVaultFlow.asStateFlow() - override val vaultDataStateFlow: StateFlow> = combine( decryptCipherListResultStateFlow, @@ -358,7 +343,7 @@ class VaultSyncManagerImpl( vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) val itemsAvailable = syncResponse.ciphers?.isNotEmpty() == true syncResponse.ciphers?.let { - verifyAndUpdateIfUserShouldMigrateVaultToMyItems(it) + vaultMigrationManager.verifyAndUpdateMigrationState(it) } SyncVaultDataResult.Success(itemsAvailable = itemsAvailable) } @@ -430,7 +415,7 @@ class VaultSyncManagerImpl( onSuccess = { result -> // We need to be sure the data on device is updated // before sending the user to the migration screen - if (userShouldMigrateVault { + if (vaultMigrationManager.shouldMigrateVault { result.successes.any { it.organizationId == null } } ) { @@ -550,44 +535,6 @@ class VaultSyncManagerImpl( } } - /** - * Evaluates if the user should migrate their personal vault based on - * policies, personal ciphers, feature flag, and network connectivity. - */ - private fun userShouldMigrateVault(hasPersonalItems: () -> Boolean): Boolean { - return policyManager - .getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - .any() && - featureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) && - connectionManager.isNetworkConnected && - hasPersonalItems() - } - - private fun verifyAndUpdateIfUserShouldMigrateVaultToMyItems(cipherList: List) { - val userId = activeUserId ?: return - - mutableShouldMigratePersonalVaultFlow.update { - if (userShouldMigrateVault { cipherList.any { it.organizationId == null } }) { - val orgId = policyManager.getPersonalOwnershipPolicyOrganizationId() - val orgName = authDiskSource - .getOrganizations(userId = userId) - ?.firstOrNull { it.id == orgId } - ?.name - - if (orgId != null && orgName != null) { - VaultMigrationData.MigrationRequired( - organizationId = orgId, - organizationName = orgName, - ) - } else { - VaultMigrationData.NoMigrationRequired - } - } else { - VaultMigrationData.NoMigrationRequired - } - } - } - /** * Returns the given [DataState] as-is, or [DataState.Loading] if vault data for the given * [userId] has not synced. This can be used to distinguish between empty data in the database diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index 256ebd33a3..130e2dc6c8 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -39,6 +39,8 @@ import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManagerImpl import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManagerImpl import dagger.Module @@ -57,6 +59,20 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object VaultManagerModule { + @Provides + @Singleton + fun provideVaultMigrationManager( + authDiskSource: AuthDiskSource, + policyManager: PolicyManager, + featureFlagManager: FeatureFlagManager, + connectionManager: NetworkConnectionManager, + ): VaultMigrationManager = VaultMigrationManagerImpl( + authDiskSource = authDiskSource, + policyManager = policyManager, + featureFlagManager = featureFlagManager, + connectionManager = connectionManager, + ) + @Provides @Singleton fun provideCipherManager( @@ -195,9 +211,7 @@ object VaultManagerModule { userLogoutManager: UserLogoutManager, userStateManager: UserStateManager, vaultLockManager: VaultLockManager, - policyManager: PolicyManager, - featureFlagManager: FeatureFlagManager, - connectionManager: NetworkConnectionManager, + vaultMigrationManager: VaultMigrationManager, clock: Clock, databaseSchemeManager: DatabaseSchemeManager, pushManager: PushManager, @@ -211,9 +225,7 @@ object VaultManagerModule { userLogoutManager = userLogoutManager, userStateManager = userStateManager, vaultLockManager = vaultLockManager, - policyManager = policyManager, - featureFlagManager = featureFlagManager, - connectionManager = connectionManager, + vaultMigrationManager = vaultMigrationManager, clock = clock, databaseSchemeManager = databaseSchemeManager, pushManager = pushManager, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index f1a907b0f0..985fb7554a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -19,8 +19,8 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData -import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine @@ -37,7 +37,7 @@ import javax.inject.Inject class RootNavViewModel @Inject constructor( private val authRepository: AuthRepository, specialCircumstanceManager: SpecialCircumstanceManager, - vaultRepository: VaultRepository, + vaultMigrationManager: VaultMigrationManager, ) : BaseViewModel( initialState = RootNavState.Splash, ) { @@ -46,7 +46,7 @@ class RootNavViewModel @Inject constructor( authRepository.authStateFlow, authRepository.userStateFlow, specialCircumstanceManager.specialCircumstanceStateFlow, - vaultRepository.shouldMigratePersonalVaultFlow, + vaultMigrationManager.vaultMigrationDataStateFlow, ) { authState, userState, specialCircumstance, shouldMigratePersonalVault -> RootNavAction.Internal.UserStateUpdateReceive( authState = authState, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt new file mode 100644 index 0000000000..1b49492f41 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt @@ -0,0 +1,424 @@ +package com.x8bit.bitwarden.data.vault.manager + +import app.cash.turbine.test +import com.bitwarden.core.data.manager.model.FlagKey +import com.bitwarden.network.model.PolicyTypeJson +import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.network.model.createMockCipher +import com.bitwarden.network.model.createMockPolicy +import com.bitwarden.network.model.createMockProfile +import com.bitwarden.network.model.createMockSyncResponse +import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager +import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager +import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.ZonedDateTime + +class VaultMigrationManagerTest { + + private val fakeAuthDiskSource = FakeAuthDiskSource() + private val policyManager: PolicyManager = mockk { + every { getActivePolicies(any()) } returns emptyList() + } + private val mockFeatureFlagManager = mockk(relaxed = true) { + every { getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) } returns true + } + private val connectionManager: NetworkConnectionManager = mockk { + every { isNetworkConnected } returns true + } + + private val vaultMigrationManager: VaultMigrationManager = VaultMigrationManagerImpl( + authDiskSource = fakeAuthDiskSource, + policyManager = policyManager, + featureFlagManager = mockFeatureFlagManager, + connectionManager = connectionManager, + ) + + @Test + fun `vaultMigrationDataStateFlow should initially emit NoMigrationRequired`() = runTest { + assertEquals( + VaultMigrationData.NoMigrationRequired, + vaultMigrationManager.vaultMigrationDataStateFlow.value, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit MigrationRequired when all conditions are met`() = + runTest { + val userId = "mockId-1" + fakeAuthDiskSource.userState = MOCK_USER_STATE + + // Setup conditions for migration + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + every { + policyManager.getPersonalOwnershipPolicyOrganizationId() + } returns "mockId-1" + + // Store organizations + val syncResponse = createMockSyncResponse(number = 1) + fakeAuthDiskSource.storeOrganizations( + userId = userId, + organizations = syncResponse.profile.organizations, + ) + + // Setup sync with personal ciphers (organizationId = null) + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + assertEquals( + VaultMigrationData.MigrationRequired( + organizationId = "mockId-1", + organizationName = "mockName-1", + ), + awaitItem(), + ) + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when no personal ownership policy`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns emptyList() + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since no policy + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when feature flag is disabled`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + every { + mockFeatureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) + } returns false + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since feature flag is disabled + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when no network connection`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + every { connectionManager.isNetworkConnected } returns false + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since no network + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when no personal ciphers exist`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + // All ciphers belong to organizations + val orgCipher = createMockCipher(number = 1).copy(organizationId = "org-id") + val cipherList = listOf(orgCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since no personal ciphers + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when cipher list is empty`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + val cipherList = emptyList() + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since cipher list is empty + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when organization ID is null`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + every { + policyManager.getPersonalOwnershipPolicyOrganizationId() + } returns null + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since organization ID is null + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when organizations list is null`() = + runTest { + val userId = "mockId-1" + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + every { + policyManager.getPersonalOwnershipPolicyOrganizationId() + } returns "mockId-1" + + // Store organizations as null in authDiskSource + val syncResponse = createMockSyncResponse( + number = 1, + profile = createMockProfile(number = 1, organizations = null), + ) + fakeAuthDiskSource.storeOrganizations(userId = userId, organizations = null) + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since organizations list is null + expectNoEvents() + } + } + + @Test + @Suppress("MaxLineLength") + fun `verifyAndUpdateMigrationState should emit NoMigrationRequired when organization not found`() = + runTest { + val userId = "mockId-1" + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + // Return a different org ID that doesn't match the one in the sync response + every { + policyManager.getPersonalOwnershipPolicyOrganizationId() + } returns "non-existent-org-id" + + // Store organizations with a different ID + val syncResponse = createMockSyncResponse(number = 1) + fakeAuthDiskSource.storeOrganizations( + userId = userId, + organizations = syncResponse.profile.organizations, + ) + + val personalCipher = createMockCipher(number = 1).copy(organizationId = null) + val cipherList = listOf(personalCipher) + + vaultMigrationManager.vaultMigrationDataStateFlow.test { + assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) + + vaultMigrationManager.verifyAndUpdateMigrationState(cipherList) + + // Should still be NoMigrationRequired since organization is not found + expectNoEvents() + } + } + + @Test + fun `shouldMigrateVault returns true when all conditions are met`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + val result = vaultMigrationManager.shouldMigrateVault { true } + + assertEquals(true, result) + } + + @Test + fun `shouldMigrateVault returns false when no policies`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns emptyList() + + val result = vaultMigrationManager.shouldMigrateVault { true } + + assertEquals(false, result) + } + + @Test + fun `shouldMigrateVault returns false when feature flag is disabled`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + every { + mockFeatureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) + } returns false + + val result = vaultMigrationManager.shouldMigrateVault { true } + + assertEquals(false, result) + } + + @Test + fun `shouldMigrateVault returns false when no network`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + every { connectionManager.isNetworkConnected } returns false + + val result = vaultMigrationManager.shouldMigrateVault { true } + + assertEquals(false, result) + } + + @Test + fun `shouldMigrateVault returns false when no personal items`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) + every { + policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) + } returns listOf(mockPolicy) + + val result = vaultMigrationManager.shouldMigrateVault { false } + + assertEquals(false, result) + } +} + +private val MOCK_USER_STATE = UserStateJson( + activeUserId = "mockId-1", + accounts = mapOf( + "mockId-1" to AccountJson( + profile = AccountJson.Profile( + userId = "mockId-1", + email = "test@bitwarden.com", + isEmailVerified = true, + name = "Bitwarden Tester", + stamp = null, + organizationId = null, + avatarColorHex = null, + hasPremium = false, + forcePasswordResetReason = null, + kdfType = null, + kdfIterations = null, + kdfMemory = null, + kdfParallelism = null, + userDecryptionOptions = null, + isTwoFactorEnabled = false, + creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), + ), + tokens = AccountTokensJson( + accessToken = "accessToken", + refreshToken = "refreshToken", + ), + settings = AccountJson.Settings( + environmentUrlData = null, + ), + ), + ), +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt index b9f2c4835a..fe50488a16 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt @@ -5,13 +5,11 @@ import app.cash.turbine.turbineScope import com.bitwarden.collections.CollectionView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager -import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.error.MissingPropertyException import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.util.asFailure import com.bitwarden.core.data.util.asSuccess -import com.bitwarden.network.model.PolicyTypeJson import com.bitwarden.network.model.SyncResponseJson import com.bitwarden.network.model.createMockCipher import com.bitwarden.network.model.createMockCollection @@ -37,10 +35,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.error.NoActiveUserException import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager -import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PushManager -import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult @@ -146,14 +141,12 @@ class VaultSyncManagerTest { private val databaseSchemeManager: DatabaseSchemeManager = mockk { every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow } - private val policyManager: PolicyManager = mockk { - every { getActivePolicies(any()) } returns emptyList() - } - private val mockFeatureFlagManager = mockk(relaxed = true) { - every { getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) } returns true - } - private val connectionManager: NetworkConnectionManager = mockk { - every { isNetworkConnected } returns true + private val mutableVaultMigrationDataStateFlow = + MutableStateFlow(VaultMigrationData.NoMigrationRequired) + private val vaultMigrationManager: VaultMigrationManager = mockk { + every { vaultMigrationDataStateFlow } returns mutableVaultMigrationDataStateFlow + every { verifyAndUpdateMigrationState(any()) } just runs + every { shouldMigrateVault(any()) } returns false } private val vaultSyncManager: VaultSyncManager = VaultSyncManagerImpl( @@ -165,9 +158,7 @@ class VaultSyncManagerTest { userLogoutManager = userLogoutManager, userStateManager = userStateManager, vaultLockManager = vaultLockManager, - policyManager = policyManager, - featureFlagManager = mockFeatureFlagManager, - connectionManager = connectionManager, + vaultMigrationManager = vaultMigrationManager, clock = clock, databaseSchemeManager = databaseSchemeManager, pushManager = pushManager, @@ -1267,390 +1258,6 @@ class VaultSyncManagerTest { coVerify(exactly = 0) { syncService.sync() } } - @Test - fun `shouldMigratePersonalVaultFlow should initially emit false`() = runTest { - assertEquals( - VaultMigrationData.NoMigrationRequired, - vaultSyncManager.shouldMigratePersonalVaultFlow.value, - ) - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit true after sync when all conditions are met`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - // Setup conditions for migration - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - every { - policyManager.getPersonalOwnershipPolicyOrganizationId() - } returns "mockId-1" - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - // Setup sync with personal ciphers (organizationId = null) - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - assertEquals( - VaultMigrationData.MigrationRequired( - organizationId = "mockId-1", - organizationName = "mockName-1", - ), - awaitItem(), - ) - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when no personal ownership policy`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns emptyList() - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be false since no policy - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when feature flag is disabled`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - - every { - mockFeatureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) - } returns false - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be false since feature flag is disabled - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when no network connection`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - - every { connectionManager.isNetworkConnected } returns false - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be false since no network - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when no personal ciphers exist`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - - // All ciphers belong to organizations - val orgCipher = createMockCipher(number = 1).copy(organizationId = "org-id") - val syncResponse = createMockSyncResponse(number = 1).copy(ciphers = listOf(orgCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be false since no personal ciphers - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when cipher list is null`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - - val syncResponse = createMockSyncResponse(number = 1).copy(ciphers = null) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be false since ciphers is null - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when organization ID is null`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - every { - policyManager.getPersonalOwnershipPolicyOrganizationId() - } returns null - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be NoMigrationRequired since organization ID is null - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when organizations list is null`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - every { - policyManager.getPersonalOwnershipPolicyOrganizationId() - } returns "mockId-1" - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = createMockSyncResponse( - number = 1, - profile = createMockProfile(number = 1, organizations = null), - ).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = emptyMap(), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be NoMigrationRequired since organizations list is null - expectNoEvents() - } - } - - @Test - fun `shouldMigratePersonalVaultFlow should emit false when organization not found`() = - runTest { - val userId = "mockId-1" - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId) - - val mockPolicy = createMockPolicy(number = 1, type = PolicyTypeJson.PERSONAL_OWNERSHIP) - every { - policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP) - } returns listOf(mockPolicy) - every { - policyManager.getPersonalOwnershipPolicyOrganizationId() - } returns "non-existent-org-id" - - val personalCipher = createMockCipher(number = 1).copy(organizationId = null) - val syncResponse = - createMockSyncResponse(number = 1).copy(ciphers = listOf(personalCipher)) - coEvery { syncService.sync() } returns syncResponse.asSuccess() - coEvery { vaultDiskSource.replaceVaultData(userId = userId, vault = any()) } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(number = 1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - - setupEmptyDecryptionResults(userId) - setupVaultDiskSourceFlows(userId) - - vaultSyncManager.shouldMigratePersonalVaultFlow.test { - assertEquals(VaultMigrationData.NoMigrationRequired, awaitItem()) - - vaultSyncManager.sync() - - // Should still be NoMigrationRequired since organization is not found - expectNoEvents() - } - } - //region Helper functions /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 83d9667507..e40d8a175a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -28,8 +28,8 @@ 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.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager import com.x8bit.bitwarden.data.vault.manager.model.VaultMigrationData -import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import io.mockk.every import io.mockk.mockk @@ -63,10 +63,10 @@ class RootNavViewModelTest : BaseViewModelTest() { dispatcherManager = FakeDispatcherManager(), ) - private val mutableShouldMigratePersonalVaultFlow = + private val mutableVaultMigrationDataStateFlow = MutableStateFlow(VaultMigrationData.NoMigrationRequired) - private val vaultRepository = mockk { - every { shouldMigratePersonalVaultFlow } returns mutableShouldMigratePersonalVaultFlow + private val vaultMigrationManager = mockk { + every { vaultMigrationDataStateFlow } returns mutableVaultMigrationDataStateFlow } @BeforeEach @@ -1589,8 +1589,8 @@ class RootNavViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `when shouldMigratePersonalVaultFlow emits true the nav state should be MigrateToMyItems`() { - mutableShouldMigratePersonalVaultFlow.value = MOCK_VAULT_MIGRATION_DATA + fun `when vaultMigrationDataStateFlow emits true the nav state should be MigrateToMyItems`() { + mutableVaultMigrationDataStateFlow.value = MOCK_VAULT_MIGRATION_DATA mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE) val viewModel = createViewModel() @@ -1605,7 +1605,7 @@ class RootNavViewModelTest : BaseViewModelTest() { @Test fun `when vault is locked MigrateToMyItems should not show even if flow emits true`() { - mutableShouldMigratePersonalVaultFlow.value = MOCK_VAULT_MIGRATION_DATA + mutableVaultMigrationDataStateFlow.value = MOCK_VAULT_MIGRATION_DATA // Vault is locked mutableUserStateFlow.tryEmit( @@ -1624,8 +1624,8 @@ class RootNavViewModelTest : BaseViewModelTest() { } @Test - fun `when shouldMigratePersonalVaultFlow emits false MigrateToMyItems should not show`() { - mutableShouldMigratePersonalVaultFlow.value = VaultMigrationData.NoMigrationRequired + fun `when vaultMigrationDataStateFlow emits false MigrateToMyItems should not show`() { + mutableVaultMigrationDataStateFlow.value = VaultMigrationData.NoMigrationRequired mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE) val viewModel = createViewModel() @@ -1639,7 +1639,7 @@ class RootNavViewModelTest : BaseViewModelTest() { RootNavViewModel( authRepository = authRepository, specialCircumstanceManager = specialCircumstanceManager, - vaultRepository = vaultRepository, + vaultMigrationManager = vaultMigrationManager, ) }