[PM-19309] Handle restrict item types policy (#5357)

This commit is contained in:
André Bispo
2025-06-25 15:46:44 +00:00
committed by GitHub
parent 1234898786
commit fbfcfcd683
19 changed files with 869 additions and 41 deletions
@@ -37,6 +37,7 @@ sealed class FlagKey<out T : Any> {
RestrictCipherItemDeletion,
PreAuthSettings,
UserManagedPrivilegedApps,
RemoveCardPolicy,
)
}
}
@@ -181,6 +182,15 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable the removal of card item types.
* This flag will hide card types from organizations with policy enable and individual vaults
*/
data object RemoveCardPolicy : FlagKey<Boolean>() {
override val keyName: String = "pm-16442-remove-card-item-type-policy"
override val defaultValue: Boolean = false
}
//region Dummy keys for testing
/**
* Data object holding the key for a [Boolean] flag to be used in tests.
@@ -42,6 +42,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.RestrictCipherItemDeletion,
FlagKey.PreAuthSettings,
FlagKey.UserManagedPrivilegedApps,
FlagKey.RemoveCardPolicy,
-> {
@Suppress("UNCHECKED_CAST")
BooleanFlagItem(
@@ -105,4 +106,5 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.UserManagedPrivilegedApps -> {
stringResource(R.string.user_trusted_privileged_app_management)
}
FlagKey.RemoveCardPolicy -> stringResource(R.string.remove_card_policy)
}
@@ -11,7 +11,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.map
import com.bitwarden.core.util.persistentListOfNotNull
import com.bitwarden.data.repository.util.baseIconUrl
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.fido.Fido2CredentialAutofillView
@@ -49,11 +48,13 @@ import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
import com.x8bit.bitwarden.data.credentials.util.getCreatePasskeyCredentialRequestOrNull
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
@@ -93,6 +94,7 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toSendItemType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toVaultItemCipherType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
@@ -102,7 +104,10 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -111,6 +116,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import javax.inject.Inject
import kotlin.collections.map
/**
* Manages [VaultItemListingState], handles [VaultItemListingsAction],
@@ -136,6 +142,7 @@ class VaultItemListingViewModel @Inject constructor(
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val organizationEventManager: OrganizationEventManager,
private val networkConnectionManager: NetworkConnectionManager,
private val featureFlagManager: FeatureFlagManager,
private val snackbarRelayManager: SnackbarRelayManager,
private val relyingPartyParser: RelyingPartyParser,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
@@ -165,6 +172,7 @@ class VaultItemListingViewModel @Inject constructor(
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
restrictItemTypesPolicyOrgIds = persistentListOf(),
autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull(),
hasMasterPassword = userState.activeAccount.hasMasterPassword,
totpData = specialCircumstance?.toTotpDataOrNull(),
@@ -195,6 +203,21 @@ class VaultItemListingViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.RemoveCardPolicy),
) { policies, enabledFlag ->
if (enabledFlag) {
policies.map { it.organizationId }
} else {
emptyList()
}
}
.map { VaultItemListingsAction.Internal.RestrictItemTypesPolicyUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.SEND_DELETED, SnackbarRelay.SEND_UPDATED)
.map { VaultItemListingsAction.Internal.SnackbarDataReceived(it) }
@@ -763,16 +786,29 @@ class VaultItemListingViewModel @Inject constructor(
)
}
private fun createVaultItemTypeSelectionExcludedOptions(): ImmutableList<CreateVaultItemType> {
// If policy is enable for any organization, exclude the card option
return if (state.restrictItemTypesPolicyOrgIds.isNotEmpty()) {
persistentListOf(
CreateVaultItemType.CARD,
CreateVaultItemType.FOLDER,
CreateVaultItemType.SSH_KEY,
)
} else {
persistentListOf(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.FOLDER,
)
}
}
private fun handleAddVaultItemClick() {
when (val itemListingType = state.itemListingType) {
is VaultItemListingState.ItemListingType.Vault.Collection -> {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection(
excludedOptions = persistentListOfNotNull(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.FOLDER,
),
excludedOptions = createVaultItemTypeSelectionExcludedOptions(),
),
)
}
@@ -782,10 +818,7 @@ class VaultItemListingViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection(
excludedOptions = persistentListOfNotNull(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.FOLDER,
),
excludedOptions = createVaultItemTypeSelectionExcludedOptions(),
),
)
}
@@ -1427,12 +1460,32 @@ class VaultItemListingViewModel @Inject constructor(
handleGetCredentialEntriesResultReceive(action)
}
is VaultItemListingsAction.Internal.RestrictItemTypesPolicyUpdateReceive -> {
handleRestrictItemTypesPolicyUpdateReceive(action)
}
is VaultItemListingsAction.Internal.SnackbarDataReceived -> {
handleSnackbarDataReceived(action)
}
}
}
private fun handleRestrictItemTypesPolicyUpdateReceive(
action: VaultItemListingsAction.Internal.RestrictItemTypesPolicyUpdateReceive,
) {
mutableStateFlow.update {
it.copy(
restrictItemTypesPolicyOrgIds = action
.restrictItemTypesPolicyOrdIds
.toImmutableList(),
)
}
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
updateStateWithVaultData(vaultData, clearDialogState = false)
}
}
private fun handleInternetConnectionErrorReceived() {
mutableStateFlow.update {
it.copy(
@@ -2119,6 +2172,7 @@ class VaultItemListingViewModel @Inject constructor(
.fido2CredentialAutofillViewList,
totpData = state.totpData,
isPremiumUser = state.isPremium,
restrictItemTypesPolicyOrgIds = state.restrictItemTypesPolicyOrgIds,
)
}
@@ -2263,6 +2317,7 @@ data class VaultItemListingState(
val isIconLoadingDisabled: Boolean,
val dialogState: DialogState?,
val policyDisablesSend: Boolean,
val restrictItemTypesPolicyOrgIds: ImmutableList<String>,
// Internal
private val isPullToRefreshSettingEnabled: Boolean,
val totpData: TotpData? = null,
@@ -2278,8 +2333,13 @@ data class VaultItemListingState(
* Whether or not the add FAB should be shown.
*/
val hasAddItemFabButton: Boolean
get() = itemListingType.hasFab ||
(viewState is ViewState.NoItems && viewState.shouldShowAddButton)
get() = if (restrictItemTypesPolicyOrgIds.isNotEmpty() &&
itemListingType == VaultItemListingState.ItemListingType.Vault.Card) {
false
} else {
itemListingType.hasFab ||
(viewState as? ViewState.NoItems)?.shouldShowAddButton == true
}
/**
* Whether or not this represents a listing screen for autofill.
@@ -3160,6 +3220,13 @@ sealed class VaultItemListingsAction {
val policyDisablesSend: Boolean,
) : Internal()
/**
* Indicates that a restrict item types policy update has been received.
*/
data class RestrictItemTypesPolicyUpdateReceive(
val restrictItemTypesPolicyOrdIds: List<String>,
) : Internal()
/**
* Indicates that a credential creation request has been received from the
* CredentialManager.
@@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.vault.feature.util.toFolderDisplayName
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.applyRestrictItemTypesPolicy
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import com.x8bit.bitwarden.ui.vault.model.TotpData
@@ -111,11 +112,13 @@ fun VaultData.toViewState(
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
totpData: TotpData?,
isPremiumUser: Boolean,
restrictItemTypesPolicyOrgIds: List<String>,
): VaultItemListingState.ViewState {
val filteredCipherViewList = cipherViewList
.filter { cipherView ->
cipherView.determineListingPredicate(itemListingType)
}
.applyRestrictItemTypesPolicy(restrictItemTypesPolicyOrgIds)
.toFilteredList(vaultFilterType)
val folderList =
@@ -214,13 +217,12 @@ fun VaultData.toViewState(
}
.asText()
}
val shouldShowAddButton = when (itemListingType) {
VaultItemListingState.ItemListingType.Vault.Trash,
VaultItemListingState.ItemListingType.Vault.SshKey,
-> false
else -> true
}
val restrictItemTypePolicyEnabled = restrictItemTypesPolicyOrgIds.isNotEmpty() &&
itemListingType == VaultItemListingState.ItemListingType.Vault.Card
val shouldShowAddButton = !restrictItemTypePolicyEnabled && itemListingType.hasFab
VaultItemListingState.ViewState.NoItems(
header = totpData
?.let { R.string.no_items_for_vault.asText(it.issuer ?: it.accountName ?: "--") },
@@ -36,7 +36,7 @@ private const val TRASH_TYPES_COUNT: Int = 1
* Content view for the [VaultScreen].
*/
@Composable
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun VaultContent(
state: VaultState.ViewState.Content,
vaultHandlers: VaultHandlers,
@@ -183,20 +183,22 @@ fun VaultContent(
)
}
item {
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = BitwardenDrawable.ic_payment_card),
startIconTestTag = "CardCipherIcon",
label = stringResource(id = R.string.type_card),
supportingLabel = state.cardItemsCount.toString(),
onClick = vaultHandlers.cardGroupClick,
showDivider = false,
cardStyle = CardStyle.Middle(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CardFilter")
.standardHorizontalMargin(),
)
if (state.showCardGroup) {
item {
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = BitwardenDrawable.ic_payment_card),
startIconTestTag = "CardCipherIcon",
label = stringResource(id = R.string.type_card),
supportingLabel = state.cardItemsCount.toString(),
onClick = vaultHandlers.cardGroupClick,
showDivider = false,
cardStyle = CardStyle.Middle(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CardFilter")
.standardHorizontalMargin(),
)
}
}
item {
@@ -422,11 +422,12 @@ private fun VaultDialogs(
onDismissRequest = vaultHandlers.dialogDismiss,
)
VaultState.DialogState.SelectVaultAddItemType -> VaultItemSelectionDialog(
is VaultState.DialogState.SelectVaultAddItemType -> VaultItemSelectionDialog(
onOptionSelected = {
vaultHandlers.addItemClickAction(it)
},
onDismissRequest = vaultHandlers.dialogDismiss,
excludedOptions = dialogState.excludedOptions,
)
null -> Unit
@@ -72,6 +72,8 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import javax.inject.Inject
import kotlin.collections.emptyList
import kotlin.collections.map
/**
* Manages [VaultState], handles [VaultAction], and launches [VaultEvent] for the [VaultScreen].
@@ -120,6 +122,7 @@ class VaultViewModel @Inject constructor(
flightRecorderSnackBar = settingsRepository
.flightRecorderData
.toSnackbarData(clock = clock),
restrictItemTypesPolicyOrgIds = emptyList(),
)
},
) {
@@ -177,6 +180,21 @@ class VaultViewModel @Inject constructor(
.map { VaultAction.Internal.FlightRecorderDataReceive(data = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.RemoveCardPolicy),
) { policies, enabledFlag ->
if (enabledFlag && policies.isNotEmpty()) {
policies.map { it.organizationId }
} else {
null
}
}
.map { VaultAction.Internal.PolicyUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: VaultAction) {
@@ -233,9 +251,20 @@ class VaultViewModel @Inject constructor(
}
private fun handleSelectAddItemType() {
// If policy is enable for any organization, exclude the card option
var excludedOptions =
if (!state.restrictItemTypesPolicyOrgIds.isNullOrEmpty()) {
persistentListOf(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.CARD,
)
} else {
persistentListOf(CreateVaultItemType.SSH_KEY)
}
mutableStateFlow.update {
it.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType,
dialog = VaultState.DialogState.SelectVaultAddItemType(excludedOptions),
)
}
}
@@ -647,6 +676,20 @@ class VaultViewModel @Inject constructor(
is VaultAction.Internal.FlightRecorderDataReceive -> {
handleFlightRecorderDataReceive(action)
}
is VaultAction.Internal.PolicyUpdateReceive -> {
handlePolicyUpdateReceive(action)
}
}
}
private fun handlePolicyUpdateReceive(action: VaultAction.Internal.PolicyUpdateReceive) {
mutableStateFlow.update {
it.copy(restrictItemTypesPolicyOrgIds = action.restrictItemTypesPolicyOrdIds)
}
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
updateVaultState(vaultData, clearDialog = false)
}
}
@@ -767,6 +810,7 @@ class VaultViewModel @Inject constructor(
errorTitle = R.string.an_error_has_occurred.asText(),
errorMessage = R.string.generic_error_message.asText(),
isRefreshing = false,
restrictItemTypesPolicyOrgIds = state.restrictItemTypesPolicyOrgIds,
)
}
@@ -783,6 +827,7 @@ class VaultViewModel @Inject constructor(
private fun updateVaultState(
vaultData: VaultData,
clearDialog: Boolean = true,
) {
mutableStateFlow.update {
it.copy(
@@ -792,8 +837,9 @@ class VaultViewModel @Inject constructor(
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
restrictItemTypesPolicyOrgIds = state.restrictItemTypesPolicyOrgIds,
),
dialog = null,
dialog = if (clearDialog) null else state.dialog,
isRefreshing = false,
)
}
@@ -826,6 +872,7 @@ class VaultViewModel @Inject constructor(
isPremium = state.isPremium,
hasMasterPassword = state.hasMasterPassword,
vaultFilterType = vaultFilterTypeOrDefault,
restrictItemTypesPolicyOrgIds = state.restrictItemTypesPolicyOrgIds,
),
)
}
@@ -931,6 +978,7 @@ data class VaultState(
private val isPullToRefreshSettingEnabled: Boolean,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
val restrictItemTypesPolicyOrgIds: List<String>?,
) : Parcelable {
/**
@@ -1025,6 +1073,7 @@ data class VaultState(
val noFolderItems: List<VaultItem>,
val collectionItems: List<CollectionItem>,
val trashItemsCount: Int,
val showCardGroup: Boolean,
) : ViewState() {
override val hasFab: Boolean get() = true
override val isPullToRefreshEnabled: Boolean get() = true
@@ -1246,7 +1295,9 @@ data class VaultState(
* Represents a dialog for selecting a vault item type to add.
*/
@Parcelize
data object SelectVaultAddItemType : DialogState()
data class SelectVaultAddItemType(
val excludedOptions: ImmutableList<CreateVaultItemType>,
) : DialogState()
/**
* Represents an error dialog with the given [title] and [message].
@@ -1612,6 +1663,13 @@ sealed class VaultAction {
data class FlightRecorderDataReceive(
val data: FlightRecorderDataSet,
) : Internal()
/**
* Indicates that a policy update has been received.
*/
data class PolicyUpdateReceive(
val restrictItemTypesPolicyOrdIds: List<String>?,
) : Internal()
}
}
@@ -1626,6 +1684,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
errorTitle: Text,
errorMessage: Text,
isRefreshing: Boolean,
restrictItemTypesPolicyOrgIds: List<String>?,
) {
this.update {
if (vaultData != null) {
@@ -1636,6 +1695,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
hasMasterPassword = hasMasterPassword,
vaultFilterType = vaultFilterType,
isIconLoadingDisabled = isIconLoadingDisabled,
restrictItemTypesPolicyOrgIds = restrictItemTypesPolicyOrgIds,
),
dialog = VaultState.DialogState.Error(
title = errorTitle,
@@ -40,10 +40,13 @@ fun VaultData.toViewState(
isIconLoadingDisabled: Boolean,
baseIconUrl: String,
vaultFilterType: VaultFilterType,
restrictItemTypesPolicyOrgIds: List<String>?,
): VaultState.ViewState {
val filteredCipherViewListWithDeletedItems =
cipherViewList.toFilteredList(vaultFilterType)
cipherViewList
.applyRestrictItemTypesPolicy(restrictItemTypesPolicyOrgIds ?: emptyList())
.toFilteredList(vaultFilterType)
val filteredCipherViewList = filteredCipherViewListWithDeletedItems
.filter { it.deletedDate == null }
@@ -70,6 +73,7 @@ fun VaultData.toViewState(
val totpItems = filteredCipherViewList.filter { it.login?.totp != null }
val shouldShowUnGroupedItems = filteredCollectionViewList.isEmpty() &&
noFolderItems.size < NO_FOLDER_ITEM_THRESHOLD
val cardCount = filteredCipherViewList.count { it.type == CipherType.CARD }
VaultState.ViewState.Content(
itemTypesCount = itemTypesCount,
totpItemsCount = if (isPremium) {
@@ -78,7 +82,7 @@ fun VaultData.toViewState(
totpItems.count { it.organizationUseTotp }
},
loginItemsCount = filteredCipherViewList.count { it.type == CipherType.LOGIN },
cardItemsCount = filteredCipherViewList.count { it.type == CipherType.CARD },
cardItemsCount = cardCount,
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
secureNoteItemsCount = filteredCipherViewList
.count { it.type == CipherType.SECURE_NOTE },
@@ -145,6 +149,7 @@ fun VaultData.toViewState(
trashItemsCount = filteredCipherViewListWithDeletedItems.count {
it.deletedDate != null
},
showCardGroup = cardCount != 0 || restrictItemTypesPolicyOrgIds == null,
)
}
}
@@ -356,3 +361,26 @@ fun List<CollectionView>.toFilteredList(
}
}
}
/**
* Filters out [CipherType.CARD] [CipherView]s that are in [restrictItemTypesPolicyOrgIds] list.
* When [restrictItemTypesPolicyOrgIds] is not empty, individual vault items are also removed.
*/
fun List<CipherView>.applyRestrictItemTypesPolicy(
restrictItemTypesPolicyOrgIds: List<String>,
): List<CipherView> =
this
.filterNot { cipherView ->
if (restrictItemTypesPolicyOrgIds.isEmpty()) {
// No policy, so don't apply removal
false
} else if (cipherView.type != CipherType.CARD) {
// Policy only for cards
false
} else {
// If a policy is enable for a given organization then
// also hide cards from individual vault
cipherView.organizationId.isNullOrEmpty() ||
restrictItemTypesPolicyOrgIds.contains(cipherView.organizationId)
}
}
@@ -31,5 +31,6 @@
<string name="generate_error_report">Generate error report</string>
<string name="error_reports">Error reports</string>
<string name="user_trusted_privileged_app_management">User-trusted privileged app management</string>
<string name="remove_card_policy">Remove Card Policy</string>
<!-- /Debug Menu -->
</resources>
@@ -73,6 +73,10 @@ class FlagKeyTest {
FlagKey.UserManagedPrivilegedApps.keyName,
"pm-18970-user-managed-privileged-apps",
)
assertEquals(
FlagKey.RemoveCardPolicy.keyName,
"pm-16442-remove-card-item-type-policy",
)
}
@Test
@@ -95,6 +99,7 @@ class FlagKeyTest {
FlagKey.RestrictCipherItemDeletion,
FlagKey.PreAuthSettings,
FlagKey.UserManagedPrivilegedApps,
FlagKey.RemoveCardPolicy,
).all {
!it.defaultValue
},
@@ -160,6 +160,7 @@ private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
FlagKey.RestrictCipherItemDeletion to true,
FlagKey.PreAuthSettings to true,
FlagKey.UserManagedPrivilegedApps to true,
FlagKey.RemoveCardPolicy to true,
)
private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf(
@@ -179,6 +180,7 @@ private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
FlagKey.RestrictCipherItemDeletion to false,
FlagKey.PreAuthSettings to false,
FlagKey.UserManagedPrivilegedApps to false,
FlagKey.RemoveCardPolicy to false,
)
private val DEFAULT_STATE = DebugMenuState(
@@ -2384,6 +2384,7 @@ private val DEFAULT_STATE = VaultItemListingState(
hasMasterPassword = true,
isPremium = false,
isRefreshing = false,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy(
@@ -23,6 +23,7 @@ import com.bitwarden.data.repository.model.Environment
import com.bitwarden.data.repository.util.baseIconUrl
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.send.SendType
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.icon.model.IconData
@@ -60,6 +61,7 @@ import com.x8bit.bitwarden.data.credentials.model.createMockFido2CredentialAsser
import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
@@ -67,6 +69,7 @@ import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingMa
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
@@ -197,9 +200,14 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
authRepository = mockAuthRepository,
dispatcherManager = FakeDispatcherManager(),
)
private val mutableActivePoliciesFlow: MutableStateFlow<List<SyncResponseJson.Policy>> =
MutableStateFlow(emptyList())
private val policyManager: PolicyManager = mockk {
every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList()
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
every {
getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
} returns mutableActivePoliciesFlow
}
private val bitwardenCredentialManager: BitwardenCredentialManager = mockk {
every { isUserVerified } returns false
@@ -232,6 +240,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
coEvery { addTrustedPrivilegedApp(any(), any()) } just runs
}
private val mutableRemoveCardPolicyFeatureFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.RemoveCardPolicy)
} returns mutableRemoveCardPolicyFeatureFlow
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle
get() = createSavedStateHandleWithVaultItemListingType(
@@ -346,6 +361,64 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `RESTRICT_ITEM_TYPES policy changes should update restrictItemTypesPolicyOrgIds accordingly if RemoveCardPolicy flag is enable`() =
runTest {
mutableRemoveCardPolicyFeatureFlow.value = true
val viewModel = createVaultItemListingViewModel()
assertEquals(
initialState.copy(restrictItemTypesPolicyOrgIds = persistentListOf()),
viewModel.stateFlow.value,
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
assertEquals(
initialState.copy(
restrictItemTypesPolicyOrgIds = persistentListOf("Test Organization"),
),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `RESTRICT_ITEM_TYPES policy changes should update restrictItemTypesPolicyOrgIds accordingly if RemoveCardPolicy flag is disabled`() =
runTest {
val viewModel = createVaultItemListingViewModel()
assertEquals(
initialState,
viewModel.stateFlow.value,
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
assertEquals(
initialState.copy(restrictItemTypesPolicyOrgIds = persistentListOf()),
viewModel.stateFlow.value,
)
}
@Test
fun `on LockAccountClick should call lockVault for the given account`() {
val accountUserId = "userId"
@@ -1227,6 +1300,108 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `AddVaultItemClick inside a collection should show item selection dialog state`() {
val viewModel = createVaultItemListingViewModel(
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Collection(collectionId = "id"),
),
)
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
assertEquals(
createVaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Vault.Collection(
collectionId = "id",
),
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection(
excludedOptions = persistentListOf(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.FOLDER,
),
),
),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `AddVaultItemClick inside a folder should hide card item selection dialog state when RESTRICT_ITEM_TYPES policy is enabled`() =
runTest {
mutableRemoveCardPolicyFeatureFlow.value = true
val viewModel = createVaultItemListingViewModel(
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Folder(folderId = "id"),
),
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
assertEquals(
createVaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder(
folderId = "id",
),
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection(
excludedOptions = persistentListOf(
CreateVaultItemType.CARD,
CreateVaultItemType.FOLDER,
CreateVaultItemType.SSH_KEY,
),
),
).copy(restrictItemTypesPolicyOrgIds = persistentListOf("Test Organization")),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `AddVaultItemClick inside a collection should hide card item selection dialog state when RESTRICT_ITEM_TYPES policy is enabled`() =
runTest {
mutableRemoveCardPolicyFeatureFlow.value = true
val viewModel = createVaultItemListingViewModel(
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Collection(collectionId = "id"),
),
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
assertEquals(
createVaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Vault.Collection(
collectionId = "id",
),
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection(
excludedOptions = persistentListOf(
CreateVaultItemType.CARD,
CreateVaultItemType.FOLDER,
CreateVaultItemType.SSH_KEY,
),
),
).copy(restrictItemTypesPolicyOrgIds = persistentListOf("Test Organization")),
viewModel.stateFlow.value,
)
}
@Test
fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest {
val viewModel = createVaultItemListingViewModel()
@@ -5112,6 +5287,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
originManager = originManager,
networkConnectionManager = networkConnectionManager,
privilegedAppRepository = privilegedAppRepository,
featureFlagManager = featureFlagManager,
snackbarRelayManager = snackbarRelayManager,
relyingPartyParser = relyingPartyParser,
)
@@ -5140,6 +5316,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
createCredentialRequest = null,
isPremium = true,
isRefreshing = false,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
}
@@ -470,6 +470,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
)
assertEquals(
@@ -563,6 +564,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
)
assertEquals(
@@ -649,6 +651,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
)
assertEquals(
@@ -715,6 +718,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -738,6 +742,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -759,6 +764,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -781,6 +787,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -802,6 +809,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -823,6 +831,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -844,6 +853,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -869,6 +879,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -894,6 +905,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
@@ -920,6 +932,7 @@ class VaultItemListingDataExtensionsTest {
every { issuer } returns "issuer"
},
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
),
)
}
@@ -1162,6 +1175,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
)
assertEquals(
@@ -1206,6 +1220,7 @@ class VaultItemListingDataExtensionsTest {
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = emptyList(),
)
assertEquals(
@@ -1228,4 +1243,117 @@ class VaultItemListingDataExtensionsTest {
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should properly filter cards when cipher have organizationId in restrictItemTypesPolicyOrgIds`() {
mockkStatic(CipherView::subtitle)
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { any<CipherView>().subtitle } returns null
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
val vaultData = VaultData(
cipherViewList = listOf(
createMockCipherView(
number = 1,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.LOGIN,
),
createMockCipherView(
number = 2,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 3,
organizationId = null,
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 4,
organizationId = "another_id",
cipherType = CipherType.CARD,
),
),
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
fido2CredentialAutofillViewList = listOf(),
)
val actual = vaultData.toViewState(
itemListingType = VaultItemListingState.ItemListingType.Vault.Card,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
createCredentialRequestData = null,
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = listOf("restrict_item_type_policy_id"),
)
assertEquals(
VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(),
displayItemList = listOf(
createMockDisplayItemForCipher(
number = 4,
cipherType = CipherType.CARD,
subtitle = null,
).copy(secondSubtitleTestTag = "PasskeySite"),
),
displayFolderList = emptyList(),
),
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should set properly shouldShowAddButton false when restrictItemTypesPolicyOrgIds has values, vault type is card and there is an empty vault state`() {
mockkStatic(CipherView::subtitle)
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { any<CipherView>().subtitle } returns null
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
val vaultData = VaultData(
cipherViewList = listOf(),
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
fido2CredentialAutofillViewList = listOf(),
)
val actual = vaultData.toViewState(
itemListingType = VaultItemListingState.ItemListingType.Vault.Card,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
createCredentialRequestData = null,
fido2CredentialAutofillViews = null,
totpData = null,
isPremiumUser = true,
restrictItemTypesPolicyOrgIds = listOf("restrict_item_type_policy_id"),
)
// Card type
assertEquals(
VaultItemListingState.ViewState.NoItems(
message = R.string.no_cards.asText(),
shouldShowAddButton = false,
buttonText = R.string.new_card.asText(),
),
actual,
)
}
}
@@ -1551,6 +1551,36 @@ class VaultScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `card section should be visible based on state`() {
mutableStateFlow.update { state ->
state.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
cardItemsCount = 1,
showCardGroup = true,
),
)
}
composeTestRule
.onNodeWithText("Card")
.performScrollTo()
.assertIsDisplayed()
mutableStateFlow.update { state ->
state.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
cardItemsCount = 0,
showCardGroup = false,
),
)
}
composeTestRule
.onNodeWithText("Card")
.assertIsNotDisplayed()
}
@Test
fun `card item count should update according to state`() {
val rowText = "Card"
@@ -1857,7 +1887,11 @@ class VaultScreenTest : BitwardenComposeTest() {
@Test
fun `SelectVaultAddItemType dialog state show vault item type selection dialog`() {
mutableStateFlow.update {
it.copy(dialog = VaultState.DialogState.SelectVaultAddItemType)
it.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType(
persistentListOf(CreateVaultItemType.SSH_KEY),
),
)
}
composeTestRule
@@ -1870,10 +1904,52 @@ class VaultScreenTest : BitwardenComposeTest() {
.assertIsDisplayed()
}
@Test
fun `SelectVaultAddItemType dialog state hide vault item type selection if excluded`() {
mutableStateFlow.update {
it.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType(
persistentListOf(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.CARD,
),
),
)
}
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Type")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Card")
.filterToOne(hasAnyAncestor(isDialog()))
.assertDoesNotExist()
composeTestRule
.onAllNodesWithText("SSH key")
.filterToOne(hasAnyAncestor(isDialog()))
.assertDoesNotExist()
composeTestRule
.onAllNodesWithText("Card")
.filterToOne(hasAnyAncestor(isDialog()))
.assertDoesNotExist()
}
@Test
fun `when option is selected in SelectVaultAddItemType dialog add item action is sent`() {
mutableStateFlow.update {
it.copy(dialog = VaultState.DialogState.SelectVaultAddItemType)
it.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType(
persistentListOf(CreateVaultItemType.SSH_KEY),
),
)
}
composeTestRule
@@ -1998,6 +2074,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
isRefreshing = false,
showImportActionCard = false,
flightRecorderSnackBar = null,
restrictItemTypesPolicyOrgIds = null,
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(
@@ -2013,4 +2090,5 @@ private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultStat
totpItemsCount = 0,
itemTypesCount = 4,
sshKeyItemsCount = 0,
showCardGroup = true,
)
@@ -93,10 +93,16 @@ class VaultViewModelTest : BaseViewModelTest() {
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(text = any<String>(), toastDescriptorOverride = any<Text>()) } just runs
}
private val mutableActivePoliciesFlow: MutableStateFlow<List<SyncResponseJson.Policy>> =
MutableStateFlow(emptyList())
private val policyManager: PolicyManager = mockk {
every {
getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
} returns emptyList()
every {
getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
} returns mutableActivePoliciesFlow
}
private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false)
@@ -152,11 +158,15 @@ class VaultViewModelTest : BaseViewModelTest() {
}
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
private val mutableRemoveCardPolicyFeatureFlow = MutableStateFlow(false)
private val mutableSshKeyVaultItemsEnabledFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
} returns mutableImportLoginsFeatureFlow
every {
getFeatureFlagFlow(FlagKey.RemoveCardPolicy)
} returns mutableRemoveCardPolicyFeatureFlow
}
private val reviewPromptManager: ReviewPromptManager = mockk()
@@ -392,6 +402,62 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `RESTRICT_ITEM_TYPES policy changes should update restrictItemTypesPolicyOrgIds accordingly if RemoveCardPolicy flag is enable`() =
runTest {
mutableRemoveCardPolicyFeatureFlow.value = true
val viewModel = createViewModel()
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
assertEquals(
DEFAULT_STATE.copy(restrictItemTypesPolicyOrgIds = listOf("Test Organization")),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `RESTRICT_ITEM_TYPES policy changes should update restrictItemTypesPolicyOrgIds accordingly if RemoveCardPolicy flag is disabled`() =
runTest {
val viewModel = createViewModel()
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
assertEquals(
DEFAULT_STATE.copy(restrictItemTypesPolicyOrgIds = null),
viewModel.stateFlow.value,
)
}
@Test
fun `Flight Recorder changes should update flightRecorderSnackbar accordingly`() = runTest {
mockkStatic(FlightRecorderDataSet::toSnackbarData)
@@ -648,6 +714,7 @@ class VaultViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
),
)
.copy(
@@ -672,6 +739,7 @@ class VaultViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
),
),
viewModel.stateFlow.value,
@@ -785,6 +853,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = CipherType.entries.size,
sshKeyItemsCount = 1,
showCardGroup = true,
),
),
viewModel.stateFlow.value,
@@ -809,6 +878,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
)
val viewModel = createViewModel()
@@ -928,6 +998,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
),
viewModel.stateFlow.value,
@@ -1033,6 +1104,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
@@ -1136,6 +1208,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
dialog = null,
),
@@ -1213,6 +1286,7 @@ class VaultViewModelTest : BaseViewModelTest() {
totpItemsCount = 1,
itemTypesCount = CipherType.entries.size,
sshKeyItemsCount = 1,
showCardGroup = true,
),
),
viewModel.stateFlow.value,
@@ -2143,7 +2217,9 @@ class VaultViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.SelectAddItemType)
val expectedState = DEFAULT_STATE.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType,
dialog = VaultState.DialogState.SelectVaultAddItemType(
excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY),
),
)
assertEquals(
expectedState,
@@ -2151,6 +2227,40 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `SelectAddItemType action should set dialog state to SelectVaultAddItemType accordingly when RESTRICT_ITEM_TYPES is enabled`() =
runTest {
mutableRemoveCardPolicyFeatureFlow.value = true
val viewModel = createViewModel()
mutableActivePoliciesFlow.emit(
listOf(
SyncResponseJson.Policy(
organizationId = "Test Organization",
id = "testId",
type = PolicyTypeJson.RESTRICT_ITEM_TYPES,
isEnabled = true,
data = null,
),
),
)
viewModel.trySendAction(VaultAction.SelectAddItemType)
val expectedState = DEFAULT_STATE.copy(
dialog = VaultState.DialogState.SelectVaultAddItemType(
excludedOptions = persistentListOf(
CreateVaultItemType.SSH_KEY,
CreateVaultItemType.CARD,
),
),
restrictItemTypesPolicyOrgIds = listOf("Test Organization"),
)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
@Test
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
runTest {
@@ -2292,4 +2402,5 @@ private fun createMockVaultState(
showImportActionCard = true,
isRefreshing = false,
flightRecorderSnackBar = null,
restrictItemTypesPolicyOrgIds = null,
)
@@ -64,6 +64,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -107,6 +108,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -131,6 +133,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.MyVault,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -153,6 +156,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -186,6 +190,7 @@ class VaultDataExtensionsTest {
organizationName = "Mock Organization 1",
),
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -219,6 +224,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -239,6 +245,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -262,6 +269,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -286,6 +294,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -302,6 +311,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -323,6 +333,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -339,6 +350,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 0,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -360,6 +372,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -376,6 +389,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -399,6 +413,7 @@ class VaultDataExtensionsTest {
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -415,6 +430,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -622,6 +638,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -638,6 +655,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -661,6 +679,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -677,6 +696,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 0,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -703,6 +723,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -725,6 +746,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 100,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -752,6 +774,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -785,6 +808,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
@@ -818,6 +842,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -866,6 +891,123 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 5,
sshKeyItemsCount = 0,
showCardGroup = true,
),
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should excluded card vault items and adjust type count for ciphers with orgId in restrictItemTypesPolicyOrgIds and set showCardGroup to true if there are remaining cards`() {
val vaultData = VaultData(
cipherViewList = listOf(
createMockCipherView(
number = 1,
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 2,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 3,
organizationId = "another_id",
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 4,
organizationId = null,
cipherType = CipherType.CARD,
),
),
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
fido2CredentialAutofillViewList = null,
)
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = listOf("restrict_item_type_policy_id"),
)
assertEquals(
VaultState.ViewState.Content(
loginItemsCount = 0,
cardItemsCount = 2,
identityItemsCount = 0,
secureNoteItemsCount = 0,
sshKeyItemsCount = 0,
favoriteItems = listOf(),
collectionItems = listOf(),
folderItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 0,
itemTypesCount = CipherType.entries.size,
showCardGroup = true,
),
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState should excluded card vault items for ciphers with orgId in restrictItemTypesPolicyOrgIds and set showCardGroup to false if there are no remaining cards`() {
val vaultData = VaultData(
cipherViewList = listOf(
createMockCipherView(
number = 1,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.LOGIN,
),
createMockCipherView(
number = 2,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.CARD,
),
createMockCipherView(
number = 3,
organizationId = "restrict_item_type_policy_id",
cipherType = CipherType.CARD,
),
),
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
fido2CredentialAutofillViewList = null,
)
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = listOf("restrict_item_type_policy_id"),
)
assertEquals(
VaultState.ViewState.Content(
loginItemsCount = 1,
cardItemsCount = 0,
identityItemsCount = 0,
secureNoteItemsCount = 0,
sshKeyItemsCount = 0,
favoriteItems = listOf(),
collectionItems = listOf(),
folderItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 1,
itemTypesCount = CipherType.entries.size,
showCardGroup = false,
),
actual,
)
@@ -890,6 +1032,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -908,6 +1051,7 @@ class VaultDataExtensionsTest {
totpItemsCount = 1,
// Verify item types count includes all CipherTypes when showSshKeys is true.
itemTypesCount = CipherType.entries.size,
showCardGroup = true,
),
actual,
)
@@ -947,6 +1091,7 @@ class VaultDataExtensionsTest {
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
restrictItemTypesPolicyOrgIds = null,
)
assertEquals(
@@ -978,6 +1123,7 @@ class VaultDataExtensionsTest {
trashItemsCount = 0,
totpItemsCount = 0,
itemTypesCount = CipherType.entries.size,
showCardGroup = true,
),
actual,
)
@@ -85,5 +85,6 @@ class VaultStateExtensionsTest {
totpItemsCount = 1,
itemTypesCount = 4,
sshKeyItemsCount = 0,
showCardGroup = true,
)
}
@@ -88,6 +88,12 @@ enum class PolicyTypeJson {
@SerialName("14")
REMOVE_UNLOCK_WITH_PIN,
/**
* Restricts the types of items that can be shown in the vault.
*/
@SerialName("15")
RESTRICT_ITEM_TYPES,
/**
* Represents an unknown policy type.
*