From 87d324b0633bc3a32f21ba17f601ff743d419cef Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:42:06 -0500 Subject: [PATCH] [PM-12922] Disable delete if user can't manage collection (#4179) --- .../feature/addedit/VaultAddEditScreen.kt | 2 +- .../feature/addedit/VaultAddEditViewModel.kt | 26 +- .../addedit/util/CipherViewExtensions.kt | 4 +- .../ui/vault/feature/item/VaultItemScreen.kt | 3 +- .../vault/feature/item/VaultItemViewModel.kt | 31 ++- .../feature/item/model/VaultItemStateData.kt | 1 + .../feature/item/util/CipherViewExtensions.kt | 4 +- .../sdk/model/CollectionViewUtil.kt | 8 +- .../addedit/VaultAddEditViewModelTest.kt | 140 ++++++++++ .../addedit/util/CipherViewExtensionsTest.kt | 7 + .../vault/feature/item/VaultItemScreenTest.kt | 28 +- .../feature/item/VaultItemViewModelTest.kt | 247 ++++++++++++++++++ .../item/util/CipherViewExtensionsTest.kt | 13 + .../feature/item/util/VaultItemTestUtil.kt | 2 + 14 files changed, 503 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index 2bf3d550c..23a778d30 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -314,7 +314,7 @@ fun VaultAddEditScreen( text = stringResource(id = R.string.delete), onClick = { pendingDeleteCipher = true }, ) - .takeUnless { state.isAddItemMode }, + .takeUnless { state.isAddItemMode || !state.canDelete }, ), ) }, 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 fd7dfb4ae..c33374100 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 @@ -149,7 +149,9 @@ class VaultAddEditViewModel @Inject constructor( attestationOptions = fido2AttestationOptions, isIndividualVaultDisabled = isIndividualVaultDisabled, ) - ?: totpData?.toDefaultAddTypeContent(isIndividualVaultDisabled) + ?: totpData?.toDefaultAddTypeContent( + isIndividualVaultDisabled = isIndividualVaultDisabled, + ) ?: VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common(), isIndividualVaultDisabled = isIndividualVaultDisabled, @@ -1589,6 +1591,16 @@ class VaultAddEditViewModel @Inject constructor( currentAccount = userData?.activeAccount, vaultAddEditType = vaultAddEditType, ) { currentAccount, cipherView -> + val canDelete = vaultData.collectionViewList + .none { + val itemIsInCollection = cipherView + ?.collectionIds + ?.contains(it.id) == true + + val canManageCollection = it.manage + + itemIsInCollection && !canManageCollection + } // Derive the view state from the current Cipher for Edit mode // or use the current state for Add (cipherView @@ -1598,6 +1610,7 @@ class VaultAddEditViewModel @Inject constructor( totpData = totpData, resourceManager = resourceManager, clock = clock, + canDelete = canDelete, ) ?: viewState) .appendFolderAndOwnerData( @@ -2026,6 +2039,15 @@ data class VaultAddEditState( */ val isCloneMode: Boolean get() = vaultAddEditType is VaultAddEditType.CloneItem + /** + * Helper to determine if the UI should allow deletion of this item. + */ + val canDelete: Boolean + get() = (viewState as? ViewState.Content) + ?.common + ?.canDelete + ?: false + /** * Enum representing the main type options for the vault, such as LOGIN, CARD, etc. * @@ -2085,6 +2107,7 @@ data class VaultAddEditState( * @property selectedOwnerId The ID of the owner associated with the item. * @property availableOwners A list of available owners. * @property hasOrganizations Indicates if the user is part of any organizations. + * @property canDelete Indicates whether the current user can delete the item. */ @Parcelize data class Common( @@ -2101,6 +2124,7 @@ data class VaultAddEditState( val selectedOwnerId: String? = null, val availableOwners: List = emptyList(), val hasOrganizations: Boolean = false, + val canDelete: Boolean = true, ) : Parcelable { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index a68834588..c401ee77c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -35,13 +35,14 @@ private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a" /** * Transforms [CipherView] into [VaultAddEditState.ViewState]. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") fun CipherView.toViewState( isClone: Boolean, isIndividualVaultDisabled: Boolean, totpData: TotpData?, resourceManager: ResourceManager, clock: Clock, + canDelete: Boolean, ): VaultAddEditState.ViewState = VaultAddEditState.ViewState.Content( type = when (type) { @@ -108,6 +109,7 @@ fun CipherView.toViewState( availableOwners = emptyList(), hasOrganizations = false, customFieldData = this.fields.orEmpty().map { it.toCustomField() }, + canDelete = canDelete, ), isIndividualVaultDisabled = isIndividualVaultDisabled, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index 63011234f..c16a9a3de 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -226,7 +226,8 @@ fun VaultItemScreen( ) } }, - ), + ) + .takeIf { state.canDelete }, ), ) }, 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 b3623dbdc..29bb1affb 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 @@ -82,7 +82,8 @@ class VaultItemViewModel @Inject constructor( vaultRepository.getVaultItemStateFlow(state.vaultItemId), authRepository.userStateFlow, vaultRepository.getAuthCodeFlow(state.vaultItemId), - ) { cipherViewState, userState, authCodeState -> + vaultRepository.collectionsStateFlow, + ) { cipherViewState, userState, authCodeState, collectionsState -> val totpCodeData = authCodeState.data?.let { TotpCodeItemData( periodSeconds = it.periodSeconds, @@ -96,14 +97,30 @@ class VaultItemViewModel @Inject constructor( vaultDataState = combineDataStates( cipherViewState, authCodeState, - ) { _, _ -> + collectionsState, + ) { _, _, _ -> // We are only combining the DataStates to know the overall state, // 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 + + val canManageCollection = it.manage + + itemIsInCollection && !canManageCollection + } + ?: true + VaultItemStateData( cipher = cipherViewState.data, totpCodeItemData = totpCodeData, + canDelete = canDelete, ) }, ) @@ -915,6 +932,7 @@ class VaultItemViewModel @Inject constructor( isPremiumUser = account.isPremium, hasMasterPassword = account.hasMasterPassword, totpCodeItemData = this.data?.totpCodeItemData, + canDelete = this.data?.canDelete == true, ) ?: VaultItemState.ViewState.Error(message = errorText) @@ -1153,6 +1171,14 @@ data class VaultItemState( ?.isNotEmpty() ?: false + /** + * Whether or not the cipher can be deleted. + */ + val canDelete: Boolean + get() = viewState.asContentOrNull() + ?.common + ?.canDelete == true + /** * The text to display on the deletion confirmation dialog. */ @@ -1216,6 +1242,7 @@ data class VaultItemState( @IgnoredOnParcel val currentCipher: CipherView? = null, val attachments: List?, + val canDelete: Boolean, ) : Parcelable { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt index 91d3ba2a5..1f7c9d43b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/model/VaultItemStateData.kt @@ -11,4 +11,5 @@ import com.bitwarden.vault.CipherView data class VaultItemStateData( val cipher: CipherView?, val totpCodeItemData: TotpCodeItemData?, + val canDelete: Boolean, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index e4c46f592..565f600a9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -32,13 +32,14 @@ private const val FIDO2_CREDENTIAL_CREATION_TIME_PATTERN: String = "h:mm a" /** * Transforms [VaultData] into [VaultState.ViewState]. */ -@Suppress("CyclomaticComplexMethod", "LongMethod") +@Suppress("CyclomaticComplexMethod", "LongMethod", "LongParameterList") fun CipherView.toViewState( previousState: VaultItemState.ViewState.Content?, isPremiumUser: Boolean, hasMasterPassword: Boolean, totpCodeItemData: TotpCodeItemData?, clock: Clock = Clock.systemDefaultZone(), + canDelete: Boolean, ): VaultItemState.ViewState = VaultItemState.ViewState.Content( common = VaultItemState.ViewState.Content.Common( @@ -79,6 +80,7 @@ fun CipherView.toViewState( } } .orEmpty(), + canDelete = canDelete, ), type = when (type) { CipherType.LOGIN -> { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CollectionViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CollectionViewUtil.kt index 379ad075f..5d3e216f4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CollectionViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CollectionViewUtil.kt @@ -5,7 +5,11 @@ import com.bitwarden.vault.CollectionView /** * Create a mock [CollectionView] with a given [number]. */ -fun createMockCollectionView(number: Int, name: String? = null): CollectionView = +fun createMockCollectionView( + number: Int, + name: String? = null, + manage: Boolean = true, +): CollectionView = CollectionView( id = "mockId-$number", organizationId = "mockOrganizationId-$number", @@ -13,5 +17,5 @@ fun createMockCollectionView(number: Int, name: String? = null): CollectionView name = name ?: "mockName-$number", externalId = "mockExternalId-$number", readOnly = false, - manage = true, + manage = manage, ) 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 e62191f8a..1347c36c1 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 @@ -45,6 +45,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson 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.datasource.sdk.model.createMockSdkFido2CredentialList import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult @@ -1195,6 +1196,136 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `in edit mode, canDelete should be false when cipher is in a collection the user cannot manage`() = + runTest { + val cipherView = createMockCipherView(1) + val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) + val stateWithName = createVaultAddItemState( + vaultAddEditType = vaultAddEditType, + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", + canDelete = false, + ), + ) + + every { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = false, + ) + } returns stateWithName.viewState + + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData( + cipherView = cipherView, + collectionViewList = listOf( + createMockCollectionView( + number = 1, + manage = false, + ), + ), + ), + ) + + createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = vaultAddEditType, + ), + ) + + verify { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = false, + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in edit mode, canDelete should be true when cipher is in a collection the user can manage`() = + runTest { + val cipherView = createMockCipherView(1) + val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) + val stateWithName = createVaultAddItemState( + vaultAddEditType = vaultAddEditType, + commonContentViewState = createCommonContentViewState( + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", + canDelete = true, + ), + ) + + every { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = true, + ) + } returns stateWithName.viewState + + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData( + cipherView = cipherView, + collectionViewList = listOf( + createMockCollectionView( + number = 1, + manage = true, + ), + ), + ), + ) + + createAddVaultItemViewModel( + createSavedStateHandleWithState( + state = stateWithName, + vaultAddEditType = vaultAddEditType, + ), + ) + + verify { + cipherView.toViewState( + isClone = false, + isIndividualVaultDisabled = false, + totpData = null, + resourceManager = resourceManager, + clock = fixedClock, + canDelete = true, + ) + } + } + @Test fun `in edit mode, updateCipher success should ShowToast and NavigateBack`() = runTest { @@ -1310,6 +1441,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState mutableVaultDataFlow.value = DataState.Loaded( @@ -1341,6 +1473,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) } @@ -1375,6 +1508,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState coEvery { @@ -1437,6 +1571,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState coEvery { @@ -1502,6 +1637,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState mutableVaultDataFlow.value = DataState.Loaded( @@ -1558,6 +1694,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState every { fido2CredentialManager.isUserVerified } returns true @@ -1619,6 +1756,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { totpData = null, resourceManager = resourceManager, clock = fixedClock, + canDelete = true, ) } returns stateWithName.viewState every { fido2CredentialManager.isUserVerified } returns false @@ -3982,6 +4120,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { availableOwners: List = createOwnerList(), selectedOwnerId: String? = null, hasOrganizations: Boolean = true, + canDelete: Boolean = true, ): VaultAddEditState.ViewState.Content.Common = VaultAddEditState.ViewState.Content.Common( name = name, @@ -3995,6 +4134,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { availableFolders = availableFolders, availableOwners = availableOwners, hasOrganizations = hasOrganizations, + canDelete = canDelete, ) @Suppress("LongParameterList") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index 0d6328799..a2ddb3963 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -75,6 +75,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -121,6 +122,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -172,6 +174,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -232,6 +235,7 @@ class CipherViewExtensionsTest { totpData = mockk { every { uri } returns totp }, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -289,6 +293,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -324,6 +329,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( @@ -368,6 +374,7 @@ class CipherViewExtensionsTest { totpData = null, resourceManager = resourceManager, clock = FIXED_CLOCK, + canDelete = true, ) assertEquals( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index dbcd02847..cb008adb0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -780,22 +780,39 @@ class VaultItemScreenTest : BaseComposeTest() { } @Test - fun `menu Delete option click should be displayed`() { - - // Confirm dropdown version of item is absent + fun `menu Delete option should be displayed based on state`() { + // Confirm overflow is closed on initial load composeTestRule .onAllNodesWithText("Delete") .filter(hasAnyAncestor(isPopup())) .assertCountEquals(0) + // Open the overflow menu composeTestRule .onNodeWithContentDescription("More") .performClick() - // Click on the delete item in the dropdown + + // Confirm Delete option is present + mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) } composeTestRule .onAllNodesWithText("Delete") .filterToOne(hasAnyAncestor(isPopup())) .assertIsDisplayed() + + // Confirm Delete option is not present when canDelete is false + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_LOGIN_VIEW_STATE + .copy( + common = DEFAULT_COMMON + .copy(canDelete = false), + ), + ) + } + composeTestRule + .onAllNodesWithText("Delete") + .filter(hasAnyAncestor(isPopup())) + .assertCountEquals(0) } @Test @@ -1166,6 +1183,7 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `Menu should display correct items when cipher is not in a collection`() { + mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) } composeTestRule .onNodeWithContentDescription("More") .performClick() @@ -2374,6 +2392,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = title = "test.mp4", ), ), + canDelete = true, ) private val DEFAULT_PASSKEY = R.string.created_xy.asText( @@ -2455,6 +2474,7 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = requiresReprompt = true, requiresCloneConfirmation = false, attachments = emptyList(), + canDelete = true, ) private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = 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 560de35a1..e351a8281 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 @@ -4,6 +4,7 @@ import android.net.Uri import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.vault.CipherView +import com.bitwarden.vault.CollectionView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -17,6 +18,7 @@ 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 @@ -59,6 +61,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val mutableAuthCodeItemFlow = MutableStateFlow>(DataState.Loading) private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) + private val mutableCollectionsStateFlow = + MutableStateFlow>>(DataState.Loading) private val clipboardManager: BitwardenClipboardManager = mockk() private val authRepo: AuthRepository = mockk { @@ -67,6 +71,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val vaultRepo: VaultRepository = mockk { every { getAuthCodeFlow(VAULT_ITEM_ID) } returns mutableAuthCodeItemFlow every { getVaultItemStateFlow(VAULT_ITEM_ID) } returns mutableVaultItemFlow + every { collectionsStateFlow } returns mutableCollectionsStateFlow } private val mockFileManager: FileManager = mockk() @@ -151,6 +156,105 @@ class VaultItemViewModelTest : BaseViewModelTest() { assertEquals(initialState.copy(dialog = null), viewModel.stateFlow.value) } + @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, + ) + } 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, + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `canDelete should be false when cipher is in a collection that the user cannot manage`() = + runTest { + val mockCipherView = mockk { + every { collectionIds } returns listOf("mockId-1", "mockId-2") + every { + toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = 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), + createMockCollectionView(number = 2) + .copy(manage = true), + ), + ) + verify { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = false, + ) + } + } + + @Test + fun `canDelete should be true when cipher is not in collections`() { + val mockCipherView = mockk { + every { collectionIds } returns listOf("mockId-3") + every { + toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = 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, + ) + } + } + @Test fun `DeleteClick should show password dialog when re-prompt is required`() = runTest { @@ -162,11 +266,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) @@ -185,6 +291,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -204,6 +311,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns loginState } @@ -221,6 +329,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) assertEquals(expected, viewModel.stateFlow.value) @@ -247,6 +356,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns loginState } @@ -260,6 +370,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.DeleteClick) assertEquals(expected, viewModel.stateFlow.value) @@ -279,12 +390,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -322,11 +435,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -368,12 +483,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -406,11 +523,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -439,12 +558,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns viewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = viewState) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -476,6 +597,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns DEFAULT_VIEW_STATE } @@ -483,6 +605,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableAuthCodeItemFlow.value = DataState.Loaded( data = createVerificationCodeItem(), ) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -516,11 +639,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val viewModel = createViewModel(state = DEFAULT_STATE) coEvery { @@ -565,11 +690,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val viewModel = createViewModel(state = loginState) assertEquals(loginState, viewModel.stateFlow.value) @@ -597,11 +724,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -613,6 +742,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } assertEquals( @@ -638,6 +768,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState @@ -648,6 +779,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } returns ValidatePasswordResult.Success(isValid = true) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -699,6 +831,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState @@ -709,6 +842,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } returns ValidatePasswordResult.Success(isValid = false) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -752,6 +886,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState @@ -762,6 +897,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } returns ValidatePasswordResult.Error mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -816,12 +952,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick("field")) @@ -839,6 +977,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -854,6 +993,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) @@ -862,6 +1002,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field)) @@ -871,6 +1012,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) organizationEventManager.trackEvent( @@ -910,12 +1052,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -941,6 +1085,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -974,11 +1119,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -1004,6 +1151,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) organizationEventManager.trackEvent( @@ -1024,12 +1172,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.AttachmentsClick) @@ -1047,6 +1197,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -1066,12 +1217,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1100,12 +1253,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1125,6 +1280,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -1148,12 +1304,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CloneClick) @@ -1196,12 +1354,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.CloneClick) @@ -1219,6 +1379,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -1238,12 +1399,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1269,12 +1432,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns DEFAULT_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.Common.MoveToOrganizationClick) @@ -1292,6 +1457,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } @@ -1311,12 +1477,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) @@ -1353,12 +1521,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1409,12 +1579,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1474,12 +1646,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = any(), isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = null, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) val viewModel = createViewModel(state = loginState) @@ -1652,6 +1826,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } returns DEFAULT_VIEW_STATE @@ -1659,6 +1834,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) val breachCount = 5 @@ -1692,6 +1868,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } @@ -1710,6 +1887,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } returns DEFAULT_VIEW_STATE @@ -1717,6 +1895,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { 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) @@ -1736,6 +1915,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } @@ -1750,6 +1930,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) @@ -1757,6 +1938,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) every { clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD) } just runs @@ -1768,6 +1950,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } @@ -1782,6 +1965,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableAuthCodeItemFlow.value = DataState.Loaded( data = createVerificationCodeItem(), ) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick) @@ -1808,6 +1992,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { previousState = null, isPremiumUser = true, hasMasterPassword = true, + canDelete = true, totpCodeItemData = createTotpCodeData(), ) } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) @@ -1815,6 +2000,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME) } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick) @@ -1825,6 +2011,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } } @@ -1849,12 +2036,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } 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.PasswordHistoryClick) @@ -1873,6 +2062,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } } @@ -1888,6 +2078,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } .returns( @@ -1899,6 +2090,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.eventFlow.test { viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick) @@ -1914,6 +2106,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } } @@ -1930,12 +2123,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } 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( @@ -1958,6 +2153,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } } @@ -1977,12 +2173,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) } returns loginViewState } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(loginState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2008,6 +2206,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = createTotpCodeData(), + canDelete = true, ) organizationEventManager.trackEvent( event = OrganizationEvent.CipherClientToggledPasswordVisible( @@ -2042,11 +2241,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns CARD_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick) @@ -2067,6 +2268,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2081,6 +2283,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2090,6 +2293,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick) @@ -2100,6 +2304,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2115,11 +2320,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns CARD_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2140,6 +2347,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2154,6 +2362,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2163,6 +2372,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction( VaultItemAction.ItemType.Card.NumberVisibilityClick(isVisible = true), @@ -2179,6 +2389,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2194,11 +2405,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns CARD_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick) @@ -2219,6 +2432,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2233,6 +2447,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2242,6 +2457,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick) @@ -2252,6 +2468,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2267,11 +2484,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns CARD_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(cardState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2292,6 +2511,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2306,6 +2526,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), @@ -2314,6 +2535,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) viewModel.trySendAction( @@ -2331,6 +2553,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } } @@ -2359,11 +2582,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns SSH_KEY_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyPublicKeyClick) @@ -2387,11 +2612,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns SSH_KEY_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(sshKeyState, viewModel.stateFlow.value) viewModel.trySendAction( @@ -2422,11 +2649,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns SSH_KEY_VIEW_STATE } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.SshKey.CopyFingerprintClick) @@ -2466,12 +2695,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns viewState } val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Loaded(data = cipherView) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -2481,6 +2712,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals( DEFAULT_STATE.copy( @@ -2502,12 +2734,14 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns viewState } val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Pending(data = cipherView) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -2518,6 +2752,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = null) mutableVaultItemFlow.value = DataState.Pending(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals( DEFAULT_STATE.copy( @@ -2539,6 +2774,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns viewState } @@ -2575,6 +2811,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, totpCodeItemData = null, + canDelete = true, ) } returns viewState } @@ -2608,6 +2845,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } + @Nested + inner class CollectionsFlow { + @BeforeEach + fun setup() { + mutableUserStateFlow.value = DEFAULT_USER_STATE + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + } + } + @Suppress("LongParameterList") private fun createViewModel( state: VaultItemState?, @@ -2780,6 +3026,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { title = "test.mp4", ), ), + canDelete = true, ) private val DEFAULT_VIEW_STATE: VaultItemState.ViewState.Content = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index b2ab66549..05d86866a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -45,6 +45,7 @@ class CipherViewExtensionsTest { totpCode = "testCode", ), clock = fixedClock, + canDelete = true, ) assertEquals( @@ -80,6 +81,7 @@ class CipherViewExtensionsTest { totpCode = "testCode", ), clock = fixedClock, + canDelete = true, ) assertEquals( @@ -108,6 +110,7 @@ class CipherViewExtensionsTest { totpCode = "testCode", ), clock = fixedClock, + canDelete = true, ) assertEquals( @@ -136,6 +139,7 @@ class CipherViewExtensionsTest { totpCode = "testCode", ), clock = fixedClock, + canDelete = true, ) assertEquals( @@ -170,6 +174,7 @@ class CipherViewExtensionsTest { totpCode = "testCode", ), clock = fixedClock, + canDelete = true, ) assertEquals( @@ -194,6 +199,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -216,6 +222,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -237,6 +244,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -268,6 +276,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -304,6 +313,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -342,6 +352,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( @@ -364,6 +375,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) val expectedState = VaultItemState.ViewState.Content( @@ -384,6 +396,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, totpCodeItemData = null, clock = fixedClock, + canDelete = true, ) assertEquals( VaultItemState.ViewState.Content( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 6b0d0c0cb..5fb2db4fe 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -170,6 +170,7 @@ fun createCommonContent( requiresReprompt = true, requiresCloneConfirmation = false, attachments = emptyList(), + canDelete = true, ) } else { VaultItemState.ViewState.Content.Common( @@ -213,6 +214,7 @@ fun createCommonContent( title = "test.mp4", ), ), + canDelete = true, ) }