[PM-19309] Fix search when restrict item policy is enabled (#5497)

This commit is contained in:
André Bispo
2025-07-09 18:30:46 +01:00
committed by GitHub
parent 5f5c71979f
commit febfc82a53
3 changed files with 142 additions and 0 deletions

View File

@@ -24,10 +24,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
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.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.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
@@ -54,6 +56,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendItemType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
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.toVaultFilterData
import com.x8bit.bitwarden.ui.vault.model.TotpData
@@ -61,6 +64,11 @@ 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.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -75,6 +83,7 @@ private const val KEY_STATE = "state"
/**
* View model for the search screen.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
@HiltViewModel
class SearchViewModel @Inject constructor(
@@ -87,6 +96,7 @@ class SearchViewModel @Inject constructor(
private val organizationEventManager: OrganizationEventManager,
private val vaultRepo: VaultRepository,
private val authRepo: AuthRepository,
featureFlagManager: FeatureFlagManager,
environmentRepo: EnvironmentRepository,
settingsRepo: SettingsRepository,
snackbarRelayManager: SnackbarRelayManager,
@@ -125,6 +135,7 @@ class SearchViewModel @Inject constructor(
totpData = specialCircumstance?.toTotpDataOrNull(),
hasMasterPassword = userState.activeAccount.hasMasterPassword,
isPremium = userState.activeAccount.isPremium,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
},
) {
@@ -140,6 +151,28 @@ class SearchViewModel @Inject constructor(
.map { SearchAction.Internal.VaultDataReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
featureFlagManager
.getFeatureFlagFlow(FlagKey.RemoveCardPolicy)
.flatMapLatest { isFlagEnabled ->
if (isFlagEnabled) {
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
.map { policies ->
policies.map { it.organizationId }
}
} else {
flowOf(emptyList<String>())
}
}
.map { organizationIds ->
SearchAction.Internal.RestrictItemTypesPolicyUpdateReceive(
restrictItemTypesPolicyOrdIds = organizationIds,
)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(
SnackbarRelay.CIPHER_DELETED,
@@ -499,6 +532,10 @@ class SearchViewModel @Inject constructor(
}
is SearchAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
is SearchAction.Internal.RestrictItemTypesPolicyUpdateReceive -> {
handleRestrictItemTypesPolicyUpdateReceive(action)
}
}
}
@@ -687,6 +724,22 @@ class SearchViewModel @Inject constructor(
}
}
private fun handleRestrictItemTypesPolicyUpdateReceive(
action: SearchAction.Internal.RestrictItemTypesPolicyUpdateReceive,
) {
mutableStateFlow.update {
it.copy(
restrictItemTypesPolicyOrgIds = action
.restrictItemTypesPolicyOrdIds
.toImmutableList(),
)
}
vaultRepo.vaultDataStateFlow.value.data?.let { vaultData ->
updateStateWithVaultData(vaultData = vaultData, clearDialogState = false)
}
}
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
vaultData
.data
@@ -770,6 +823,9 @@ class SearchViewModel @Inject constructor(
vaultData
.cipherViewList
.filterAndOrganize(searchType, state.searchTerm)
.applyRestrictItemTypesPolicy(
restrictItemTypesPolicyOrgIds = state.restrictItemTypesPolicyOrgIds,
)
.toFilteredList(
vaultFilterType = state
.vaultFilterData
@@ -829,6 +885,7 @@ data class SearchState(
val totpData: TotpData?,
val hasMasterPassword: Boolean,
val isPremium: Boolean,
val restrictItemTypesPolicyOrgIds: ImmutableList<String>,
) : Parcelable {
/**
@@ -1205,6 +1262,13 @@ sealed class SearchAction {
val result: RemovePasswordSendResult,
) : Internal()
/**
* Indicates that a restrict item types policy update has been received.
*/
data class RestrictItemTypesPolicyUpdateReceive(
val restrictItemTypesPolicyOrdIds: List<String>,
) : Internal()
/**
* Indicates that snackbar data has been received.
*/

View File

@@ -45,6 +45,7 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertEquals
@@ -1028,6 +1029,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
totpData = null,
autofillSelectionData = null,
isPremium = true,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
private fun createStateForAutofill(

View File

@@ -28,12 +28,14 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySele
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
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
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.repository.EnvironmentRepository
@@ -72,6 +74,7 @@ import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
@@ -100,10 +103,16 @@ class SearchViewModelTest : 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<PolicyManager> {
every {
getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
} returns emptyList()
every {
getActivePoliciesFlow(type = PolicyTypeJson.RESTRICT_ITEM_TYPES)
} returns mutableActivePoliciesFlow
}
private val mutableVaultDataStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
@@ -141,6 +150,13 @@ class SearchViewModelTest : BaseViewModelTest() {
} returns mutableSnackbarDataFlow
}
private val mutableRemoveCardPolicyFeatureFlow = MutableStateFlow(false)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.RemoveCardPolicy)
} returns mutableRemoveCardPolicyFeatureFlow
}
@BeforeEach
fun setup() {
mockkStatic(
@@ -1584,6 +1600,64 @@ class SearchViewModelTest : 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.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(
DEFAULT_STATE.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 = 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 = persistentListOf()),
viewModel.stateFlow.value,
)
}
@Suppress("CyclomaticComplexMethod")
private fun createViewModel(
initialState: SearchState? = null,
@@ -1630,6 +1704,7 @@ class SearchViewModelTest : BaseViewModelTest() {
autofillSelectionManager = autofillSelectionManager,
organizationEventManager = organizationEventManager,
snackbarRelayManager = snackbarRelayManager,
featureFlagManager = featureFlagManager,
)
/**
@@ -1703,6 +1778,7 @@ private val DEFAULT_STATE: SearchState = SearchState(
totpData = null,
autofillSelectionData = null,
isPremium = true,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
private val DEFAULT_USER_STATE = UserState(