From d125fab0b72b35f42ac103ed160ca42d72c3791f Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:10:32 -0500 Subject: [PATCH] [PM-14843] Allow deletion of items in collections with manage permission (#4299) --- .../feature/addedit/VaultAddEditViewModel.kt | 28 +- .../vault/feature/item/VaultItemViewModel.kt | 34 +- .../feature/util/CollectionViewExtensions.kt | 33 + .../addedit/VaultAddEditViewModelTest.kt | 5 + .../feature/item/VaultItemViewModelTest.kt | 1375 +++++++---------- .../util/CollectionViewExtensionsTest.kt | 100 ++ 6 files changed, 715 insertions(+), 860 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 15ce1b3c91d..1a08cce105e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -53,6 +53,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState +import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections +import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType @@ -1579,27 +1581,13 @@ class VaultAddEditViewModel @Inject constructor( vaultAddEditType = vaultAddEditType, ) { currentAccount, cipherView -> - // Deletion is not allowed when the item is in a collection that the user - // does not have "manage" permission for. - val canDelete = vaultData.collectionViewList - .none { - val isItemInCollection = cipherView - ?.collectionIds - ?.contains(it.id) == true + val canDelete = vaultData + .collectionViewList + .hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds) - isItemInCollection && !it.manage - } - - // Assigning to a collection is not allowed when the item is in a collection - // that the user does not have "manage" and "edit" permission for. - val canAssignToCollections = vaultData.collectionViewList - .none { - val isItemInCollection = cipherView - ?.collectionIds - ?.contains(it.id) == true - - isItemInCollection && (!it.manage || it.readOnly) - } + val canAssignToCollections = vaultData + .collectionViewList + .canAssignToCollections(cipherView?.collectionIds) // Derive the view state from the current Cipher for Edit mode // or use the current state for Add diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 250f2562ebc..f119ce54549 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -28,6 +28,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState +import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections +import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import dagger.hilt.android.lifecycle.HiltViewModel @@ -103,29 +105,15 @@ class VaultItemViewModel @Inject constructor( // we map it to the appropriate value below. } .mapNullable { - // Deletion is not allowed when the item is in a collection that the user - // does not have "manage" permission for. - val canDelete = collectionsState.data - ?.none { - val itemIsInCollection = cipherViewState.data - ?.collectionIds - ?.contains(it.id) == true - - itemIsInCollection && !it.manage - } - ?: true - - // Assigning to a collection is not allowed when the item is in a collection - // that the user does not have "manage" and "edit" permission for. - val canAssignToCollections = collectionsState.data - ?.none { - val itemIsInCollection = cipherViewState.data - ?.collectionIds - ?.contains(it.id) == true - - itemIsInCollection && !it.manage && it.readOnly - } - ?: true + val canDelete = collectionsState + .data + .hasDeletePermissionInAtLeastOneCollection( + collectionIds = cipherViewState.data?.collectionIds, + ) + + val canAssignToCollections = collectionsState + .data + .canAssignToCollections(cipherViewState.data?.collectionIds) VaultItemStateData( cipher = cipherViewState.data, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt index 67870e68e44..554b2bdb1d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt @@ -88,3 +88,36 @@ fun String.toCollectionDisplayName(list: List): String { return collectionName } + +/** + * Checks if the user has delete permission in at least one collection. + * + * Deletion is allowed when the item is in any collection that the user has "manage" permission for. + */ +fun List?.hasDeletePermissionInAtLeastOneCollection(collectionIds: List?) = + this + ?.takeUnless { it.isEmpty() } + ?.any { + collectionIds + ?.contains(it.id) + ?.let { isInCollection -> !isInCollection || it.manage } + ?: true + } + ?: true + +/** + * Checks if the user has permission to assign an item to a collection. + * + * Assigning to a collection is not allowed when the item is in a collection that the user does not + * have "manage" and "edit" permission for. + */ +fun List?.canAssignToCollections(currentCollectionIds: List?) = + this + ?.none { + val itemIsInCollection = currentCollectionIds + ?.contains(it.id) + ?: false + + itemIsInCollection && (!it.manage || it.readOnly) + } + ?: true 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 7333f069b82..62e3711d172 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 @@ -1259,6 +1259,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { manage = true, readOnly = false, ), + createMockCollectionView( + number = 2, + manage = false, + readOnly = true, + ), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index dff6740ea89..89485e7a620 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -79,6 +78,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val organizationEventManager = mockk { every { trackEvent(event = any()) } just runs } + private val mockCipherView = mockk { + every { collectionIds } returns emptyList() + } @BeforeEach fun setup() { @@ -157,79 +159,11 @@ class VaultItemViewModelTest : BaseViewModelTest() { } @Test - fun `canDelete should be true when collections are empty`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `canDelete should be false when cipher is in a collection that the user cannot manage`() = + fun `DeleteClick should show password dialog when re-prompt is required`() = runTest { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-1", "mockId-2") - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false), - createMockCollectionView(number = 2) - .copy(manage = true), - ), - ) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = true, - ) - } - } - - @Test - fun `canDelete should be true when cipher is not in collections`() { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-3") + val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) every { - toViewState( + mockCipherView.toViewState( previousState = null, isPremiumUser = true, hasMasterPassword = true, @@ -238,45 +172,6 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, ) } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false), - createMockCollectionView(number = 2) - .copy(manage = false), - ), - ) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Test - fun `DeleteClick should show password dialog when re-prompt is required`() = - runTest { - val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -311,18 +206,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON .copy(requiresReprompt = false), ) - - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, canAssignToCollections = true, - ) - } returns loginState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginState val expected = DEFAULT_STATE.copy( viewState = DEFAULT_VIEW_STATE.copy( @@ -357,18 +250,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginState val expected = DEFAULT_STATE.copy( viewState = loginState, @@ -392,18 +283,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = DEFAULT_VIEW_STATE.copy( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -438,18 +327,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = DEFAULT_VIEW_STATE.copy( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -487,18 +374,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { ?.copy(deletedDate = Instant.MIN), ), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -528,18 +413,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on RestoreItemClick should prompt for master password when required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -564,18 +447,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { runTest { val viewState = DEFAULT_VIEW_STATE.copy(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -604,18 +485,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded( data = createVerificationCodeItem(), @@ -647,18 +526,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test @Suppress("MaxLineLength") fun `ConfirmRestoreClick with RestoreCipherResult Failure should should Show generic error`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -699,18 +576,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on EditClick should prompt for master password when required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -734,18 +609,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -781,17 +654,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -844,17 +716,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -899,17 +770,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -965,17 +835,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyCustomHiddenFieldClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1006,17 +874,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyCustomHiddenFieldClick should call setText on ClipboardManager when re-prompt is not required`() { val field = "field" - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = field) } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1065,17 +931,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { isCopyable = true, isVisible = false, ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1114,12 +978,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on HiddenFieldVisibilityClicked should update hidden field visibility when re-prompt is not required`() = runTest { - val hiddenField = VaultItemState.ViewState.Content.Common.Custom.HiddenField( - name = "hidden", - value = "value", - isCopyable = true, - isVisible = false, - ) + val hiddenField = + VaultItemState.ViewState.Content.Common.Custom.HiddenField( + name = "hidden", + value = "value", + isCopyable = true, + isVisible = false, + ) val loginViewState = VaultItemState.ViewState.Content( common = createCommonContent( isEmpty = true, @@ -1131,18 +996,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { type = createLoginContent(isEmpty = true), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1186,17 +1049,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on AttachmentsClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1231,17 +1092,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1267,17 +1126,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1318,17 +1175,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginState = DEFAULT_STATE.copy( viewState = loginViewState, ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1368,17 +1223,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CloneClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1413,17 +1266,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1446,17 +1297,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on MoveToOrganizationClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1491,17 +1340,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1517,74 +1365,6 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } - @Suppress("MaxLineLength") - @Test - fun `canAssignToCollections should be true when item is in a collection the user can manage and edit`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `canAssignToCollections should be false when item is not in a collection the user can manage and edit`() { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-1", "mockId-2") - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = false, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false, readOnly = true), - createMockCollectionView(number = 2) - .copy(manage = true), - ), - ) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = false, - ) - } - } - @Test fun `on CollectionsClick should emit NavigateToCollections`() = runTest { val viewModel = createViewModel(state = DEFAULT_STATE) @@ -1603,17 +1383,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = true), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1661,17 +1440,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1728,17 +1506,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1908,20 +1685,18 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CheckForBreachClick should process a password`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = - DataState.Loaded(data = createVerificationCodeItem()) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) @@ -1957,7 +1732,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } coVerify(exactly = 1) { @@ -1969,23 +1745,22 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyPasswordClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = - DataState.Loaded(data = createVerificationCodeItem()) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - - assertEquals(loginState, viewModel.stateFlow.value) + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns DEFAULT_VIEW_STATE + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = + DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + + assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick) assertEquals( loginState.copy( @@ -2004,7 +1779,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } } @@ -2012,17 +1788,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopyPasswordClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2039,7 +1814,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } } @@ -2074,20 +1850,20 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyUsernameClick should call setText on ClipboardManager`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME) } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableAuthCodeItemFlow.value = + DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick) @@ -2118,18 +1894,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on PasswordHistoryClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2162,23 +1936,21 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on PasswordHistoryClick should emit NavigateToPasswordHistory when re-prompt is not required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } - .returns( - createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - ), - ) + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) } + .returns( + createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ), + ) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2209,18 +1981,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on PasswordVisibilityClicked should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2261,18 +2031,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2331,18 +2099,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyNumberClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2375,21 +2141,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopyNumberClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2414,18 +2178,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on NumberVisibilityClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2458,21 +2220,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on NumberVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2503,18 +2263,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopySecurityCodeClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2547,21 +2305,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopySecurityCodeClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2586,18 +2342,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CodeVisibilityClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2630,21 +2384,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CodeVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2688,18 +2440,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyPublicKeyClick should copy public key to clipboard`() = runTest { every { clipboardManager.setText("mockPublicKey") } just runs - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2719,18 +2469,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2757,18 +2505,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyFingerprintClick should copy fingerprint to clipboard`() = runTest { every { clipboardManager.setText("mockFingerprint") } just runs - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2804,24 +2550,25 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Loaded and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Loaded(data = cipherView) + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) + assertEquals( + DEFAULT_STATE.copy(viewState = viewState), + viewModel.stateFlow.value, + ) } @Test @@ -2844,21 +2591,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Pending and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Pending(data = cipherView) + mutableVaultItemFlow.value = DataState.Pending(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) @@ -2885,21 +2630,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Error and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Error(error = Throwable(), data = cipherView) + mutableVaultItemFlow.value = DataState.Error(error = Throwable(), data = mockCipherView) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -2923,21 +2666,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with NoNetwork and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.NoNetwork(data = cipherView) + mutableVaultItemFlow.value = DataState.NoNetwork(data = mockCipherView) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt index acbf4f18fea..50538d7bdda 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt @@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.vault.feature.util import com.bitwarden.vault.CollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class CollectionViewExtensionsTest { @@ -66,4 +68,102 @@ class CollectionViewExtensionsTest { collectionName.toCollectionDisplayName(collectionList), ) } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the user has manage permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + + val collectionIds = listOf("mockId-1", "mockId-2") + + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return false if the user does not have manage permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = false), + createMockCollectionView(number = 2, manage = false), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertFalse(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is null`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(null.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is null`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(null)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is empty`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(emptyList())) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is empty`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue( + emptyList().hasDeletePermissionInAtLeastOneCollection( + collectionIds, + ), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `canAssociateToCollections should return true if the user has edit and manage permission`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true, readOnly = false), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(collectionList.canAssignToCollections(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `canAssociateToCollections should return false if the user does not have manage or edit permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = false, readOnly = true), + createMockCollectionView(number = 2, manage = false, readOnly = true), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertFalse(collectionList.canAssignToCollections(collectionIds)) + } + + @Test + fun `canAssociateToCollections should return true if the collectionView list is null`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(null.canAssignToCollections(collectionIds)) + } + + @Test + fun `canAssociateToCollections should return true if the collectionIds list is null`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true, readOnly = false), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.canAssignToCollections(null)) + } }