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 ece8032c0e..4d6e07fe05 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 @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import android.os.Parcelable import androidx.lifecycle.viewModelScope -import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.network.model.OrganizationType import com.bitwarden.network.util.parseJwtTokenDataOrNull import com.bitwarden.ui.platform.base.BaseViewModel @@ -18,11 +17,8 @@ import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager -import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance -import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import dagger.hilt.android.lifecycle.HiltViewModel @@ -40,10 +36,7 @@ import javax.inject.Inject class RootNavViewModel @Inject constructor( private val authRepository: AuthRepository, specialCircumstanceManager: SpecialCircumstanceManager, - private val policyManager: PolicyManager, - private val connectionManager: NetworkConnectionManager, - private val featureFlagManager: FeatureFlagManager, - private val vaultRepository: VaultRepository, + vaultRepository: VaultRepository, ) : BaseViewModel( initialState = RootNavState.Splash, ) { @@ -123,28 +116,27 @@ class RootNavViewModel @Inject constructor( } userState.activeAccount.isVaultUnlocked && - action.shouldMigratePersonalVault && - featureFlagManager.getFeatureFlag(FlagKey.MigrateMyVaultToMyItems) && - connectionManager.isNetworkConnected && - specialCircumstance == null -> { + specialCircumstance is SpecialCircumstance.AutofillSave -> { + RootNavState.VaultUnlockedForAutofillSave( + autofillSaveItem = specialCircumstance.autofillSaveItem, + ) + } + + userState.activeAccount.isVaultUnlocked && + specialCircumstance is SpecialCircumstance.AutofillSelection -> { + RootNavState.VaultUnlockedForAutofillSelection( + activeUserId = userState.activeAccount.userId, + type = specialCircumstance.autofillSelectionData.type, + ) + } + + userState.activeAccount.isVaultUnlocked && + action.shouldMigratePersonalVault -> { RootNavState.MigrateToMyItems } userState.activeAccount.isVaultUnlocked -> { when (specialCircumstance) { - is SpecialCircumstance.AutofillSave -> { - RootNavState.VaultUnlockedForAutofillSave( - autofillSaveItem = specialCircumstance.autofillSaveItem, - ) - } - - is SpecialCircumstance.AutofillSelection -> { - RootNavState.VaultUnlockedForAutofillSelection( - activeUserId = userState.activeAccount.userId, - type = specialCircumstance.autofillSelectionData.type, - ) - } - is SpecialCircumstance.AddTotpLoginItem -> { RootNavState.VaultUnlockedForNewTotp( activeUserId = userState.activeAccount.userId, @@ -227,6 +219,8 @@ class RootNavViewModel @Inject constructor( is SpecialCircumstance.CredentialExchangeExport, is SpecialCircumstance.RegistrationEvent, + is SpecialCircumstance.AutofillSave, + is SpecialCircumstance.AutofillSelection, -> { throw IllegalStateException( "Special circumstance should have been already handled.", 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 265fa64ce3..3b81627b82 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,11 +5,13 @@ 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 @@ -35,7 +37,10 @@ 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 @@ -140,6 +145,15 @@ 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 vaultSyncManager: VaultSyncManager = VaultSyncManagerImpl( syncService = syncService, @@ -150,6 +164,9 @@ class VaultSyncManagerTest { userLogoutManager = userLogoutManager, userStateManager = userStateManager, vaultLockManager = vaultLockManager, + policyManager = policyManager, + featureFlagManager = mockFeatureFlagManager, + connectionManager = connectionManager, clock = clock, databaseSchemeManager = databaseSchemeManager, pushManager = pushManager, @@ -1249,6 +1266,250 @@ class VaultSyncManagerTest { coVerify(exactly = 0) { syncService.sync() } } + @Test + fun `shouldMigratePersonalVaultFlow should initially emit false`() = runTest { + assertEquals(false, 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) + 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(false, awaitItem()) + + vaultSyncManager.sync() + + assertEquals(true, 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(false, 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(false, 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(false, 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(false, 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(false, awaitItem()) + + vaultSyncManager.sync() + + // Should still be false since ciphers is null + 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 598c782a3c..bf64db63e1 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,6 +28,7 @@ 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.repository.VaultRepository import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import io.mockk.every import io.mockk.mockk @@ -61,6 +62,11 @@ class RootNavViewModelTest : BaseViewModelTest() { dispatcherManager = FakeDispatcherManager(), ) + private val mutableShouldMigratePersonalVaultFlow = MutableStateFlow(false) + private val vaultRepository = mockk { + every { shouldMigratePersonalVaultFlow } returns mutableShouldMigratePersonalVaultFlow + } + @BeforeEach fun setup() { mockkStatic(::parseJwtTokenDataOrNull) @@ -1579,10 +1585,56 @@ class RootNavViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `when shouldMigratePersonalVaultFlow emits true the nav state should be MigrateToMyItems`() { + mutableShouldMigratePersonalVaultFlow.value = true + mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE) + val viewModel = createViewModel() + + assertEquals( + RootNavState.MigrateToMyItems, + viewModel.stateFlow.value, + ) + } + + @Test + fun `when vault is locked MigrateToMyItems should not show even if flow emits true`() { + mutableShouldMigratePersonalVaultFlow.value = true + + // Vault is locked + mutableUserStateFlow.tryEmit( + MOCK_VAULT_UNLOCKED_USER_STATE.copy( + accounts = listOf( + MOCK_VAULT_UNLOCKED_USER_STATE.activeAccount.copy(isVaultUnlocked = false), + ), + ), + ) + val viewModel = createViewModel() + + assertEquals( + RootNavState.VaultLocked, + viewModel.stateFlow.value, + ) + } + + @Test + fun `when shouldMigratePersonalVaultFlow emits false MigrateToMyItems should not show`() { + mutableShouldMigratePersonalVaultFlow.value = false + mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE) + val viewModel = createViewModel() + + assertEquals( + RootNavState.VaultUnlocked("activeUserId"), + viewModel.stateFlow.value, + ) + } + private fun createViewModel(): RootNavViewModel = RootNavViewModel( authRepository = authRepository, specialCircumstanceManager = specialCircumstanceManager, + vaultRepository = vaultRepository, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt index 26f61fcf0b..dc4465ba64 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt @@ -2,29 +2,35 @@ package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.bitwarden.data.repository.model.Environment +import com.bitwarden.network.model.OrganizationType import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.Organization +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType +import com.x8bit.bitwarden.data.platform.manager.PolicyManager +import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class MigrateToMyItemsViewModelTest : BaseViewModelTest() { - @BeforeEach - fun setup() { - mockkStatic(SavedStateHandle::toMigrateToMyItemsArgs) + private val mutableUserStateFlow = MutableStateFlow(MOCK_USER_STATE) + private val authRepository = mockk { + every { userStateFlow } returns mutableUserStateFlow } - @AfterEach - fun tearDown() { - unmockkStatic(SavedStateHandle::toMigrateToMyItemsArgs) + private val policyManager = mockk { + every { getPersonalOwnershipPolicyOrganizationId() } returns ORGANIZATION_ID } @Test @@ -150,18 +156,76 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() { } } + @Test + fun `initial state should handle null organization ID`() { + every { policyManager.getPersonalOwnershipPolicyOrganizationId() } returns null + val viewModel = createViewModel() + + assertEquals("", viewModel.stateFlow.value.organizationId) + assertEquals("", viewModel.stateFlow.value.organizationName) + assertNull(viewModel.stateFlow.value.dialog) + } + + @Test + fun `initial state should handle null organization name`() { + mutableUserStateFlow.value = MOCK_USER_STATE.copy( + accounts = listOf( + MOCK_USER_STATE.activeAccount.copy(organizations = emptyList()), + ), + ) + val viewModel = createViewModel() + + assertEquals(ORGANIZATION_ID, viewModel.stateFlow.value.organizationId) + assertEquals("", viewModel.stateFlow.value.organizationName) + assertNull(viewModel.stateFlow.value.dialog) + } + private fun createViewModel( savedStateHandle: SavedStateHandle = SavedStateHandle(), - ): MigrateToMyItemsViewModel { - every { savedStateHandle.toMigrateToMyItemsArgs() } returns MigrateToMyItemsArgs( - organizationId = ORGANIZATION_ID, - organizationName = ORGANIZATION_NAME, - ) - return MigrateToMyItemsViewModel( + ): MigrateToMyItemsViewModel = + MigrateToMyItemsViewModel( + authRepository = authRepository, + policyManager = policyManager, savedStateHandle = savedStateHandle, ) - } } private const val ORGANIZATION_ID = "test-organization-id" private const val ORGANIZATION_NAME = "Test Organization" + +private val MOCK_USER_STATE = UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "Test User", + email = "test@example.com", + avatarColorHex = "#FF0000", + environment = Environment.Us, + isPremium = false, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + needsMasterPassword = false, + hasMasterPassword = true, + trustedDevice = null, + organizations = listOf( + Organization( + id = ORGANIZATION_ID, + name = ORGANIZATION_NAME, + shouldManageResetPassword = false, + shouldUseKeyConnector = false, + role = OrganizationType.USER, + keyConnectorUrl = null, + userIsClaimedByOrganization = false, + ), + ), + isBiometricsEnabled = false, + vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = FirstTimeState(showImportLoginsCard = false), + isExportable = true, + ), + ), +)