diff options
| author | David Göransson <david.goransson@mullvad.net> | 2024-12-20 07:04:56 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-12-20 14:23:05 +0100 |
| commit | 99bd27986089de8dd05b839d2b8361cae8aefcda (patch) | |
| tree | 1fa7ab88903fe748d1c218cd0bda5bf23d4265b2 /android/app | |
| parent | 92465444d8a51b73fe225461e459449d9ab6e600 (diff) | |
| download | mullvadvpn-99bd27986089de8dd05b839d2b8361cae8aefcda.tar.xz mullvadvpn-99bd27986089de8dd05b839d2b8361cae8aefcda.zip | |
Remove one-to-one relationship between provider and ownership
Diffstat (limited to 'android/app')
17 files changed, 266 insertions, 206 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt index fa24c504af..8e4024a4d5 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt @@ -6,7 +6,6 @@ import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.Ownership import net.mullvad.mullvadvpn.lib.model.PortRange -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayList @@ -20,8 +19,8 @@ private val DUMMY_RELAY_1 = "Relay host 1", ), active = true, - provider = - Provider(providerId = ProviderId("PROVIDER RENTED"), ownership = Ownership.Rented), + provider = ProviderId("PROVIDER RENTED"), + ownership = Ownership.Rented, daita = false, ) private val DUMMY_RELAY_2 = @@ -32,8 +31,8 @@ private val DUMMY_RELAY_2 = "Relay host 2", ), active = true, - provider = - Provider(providerId = ProviderId("PROVIDER OWNED"), ownership = Ownership.MullvadOwned), + provider = ProviderId("PROVIDER OWNED"), + ownership = Ownership.MullvadOwned, daita = false, ) private val DUMMY_RELAY_CITY_1 = diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt index 2f16b27c23..4b80ea0e3c 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt @@ -11,7 +11,6 @@ import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.state.RelayFilterUiState import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -30,7 +29,7 @@ class FilterScreenTest { onApplyClick: () -> Unit = {}, onSelectedOwnership: (ownership: Ownership?) -> Unit = {}, onAllProviderCheckChange: (isChecked: Boolean) -> Unit = {}, - onSelectedProvider: (checked: Boolean, provider: Provider) -> Unit = { _, _ -> }, + onSelectedProvider: (checked: Boolean, provider: ProviderId) -> Unit = { _, _ -> }, ) { setContentWithTheme { FilterScreen( @@ -50,7 +49,7 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = DUMMY_RELAY_ALL_PROVIDERS, + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = null, selectedProviders = DUMMY_SELECTED_PROVIDERS, ) @@ -65,7 +64,7 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = DUMMY_RELAY_ALL_PROVIDERS, + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = null, selectedProviders = DUMMY_SELECTED_PROVIDERS, ) @@ -80,7 +79,7 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = DUMMY_RELAY_ALL_PROVIDERS, + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = Ownership.MullvadOwned, selectedProviders = DUMMY_SELECTED_PROVIDERS, ) @@ -95,7 +94,7 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = DUMMY_RELAY_ALL_PROVIDERS, + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = Ownership.Rented, selectedProviders = DUMMY_SELECTED_PROVIDERS, ) @@ -110,7 +109,7 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = DUMMY_RELAY_ALL_PROVIDERS, + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = null, selectedProviders = DUMMY_SELECTED_PROVIDERS, ) @@ -128,10 +127,9 @@ class FilterScreenTest { initScreen( state = RelayFilterUiState( - allProviders = listOf(), + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, selectedOwnership = null, - selectedProviders = - listOf(Provider(ProviderId("31173"), Ownership.MullvadOwned)), + selectedProviders = listOf(ProviderId("31173")), ), onApplyClick = mockClickListener, ) @@ -139,47 +137,47 @@ class FilterScreenTest { verify { mockClickListener() } } + @Test + fun ensureSelectedProviderIsShowEvenThoughItIsNotInAllProviders() = + composeExtension.use { + // Arrange + initScreen( + state = + RelayFilterUiState( + providerToOwnerships = DUMMY_RELAY_ALL_PROVIDERS, + selectedOwnership = null, + selectedProviders = listOf(ProviderId("1RemovedProvider")), + ) + ) + + // Act + onNodeWithText("Providers").performClick() + // Asset + onNodeWithText("1RemovedProvider (removed)").assertExists() + } + companion object { private val DUMMY_RELAY_ALL_PROVIDERS = - listOf( - Provider(ProviderId("31173"), Ownership.MullvadOwned), - Provider(ProviderId("100TB"), Ownership.Rented), - Provider(ProviderId("Blix"), Ownership.MullvadOwned), - Provider(ProviderId("Creanova"), Ownership.MullvadOwned), - Provider(ProviderId("DataPacket"), Ownership.Rented), - Provider(ProviderId("HostRoyale"), Ownership.Rented), - Provider(ProviderId("hostuniversal"), Ownership.Rented), - Provider(ProviderId("iRegister"), Ownership.Rented), - Provider(ProviderId("M247"), Ownership.Rented), - Provider(ProviderId("Makonix"), Ownership.Rented), - Provider(ProviderId("PrivateLayer"), Ownership.Rented), - Provider(ProviderId("ptisp"), Ownership.Rented), - Provider(ProviderId("Qnax"), Ownership.Rented), - Provider(ProviderId("Quadranet"), Ownership.Rented), - Provider(ProviderId("techfutures"), Ownership.Rented), - Provider(ProviderId("Tzulo"), Ownership.Rented), - Provider(ProviderId("xtom"), Ownership.Rented), + mapOf( + ProviderId("31173") to setOf(Ownership.MullvadOwned), + ProviderId("100TB") to setOf(Ownership.Rented), + ProviderId("Blix") to setOf(Ownership.MullvadOwned), + ProviderId("Creanova") to setOf(Ownership.MullvadOwned), + ProviderId("DataPacket") to setOf(Ownership.Rented), + ProviderId("HostRoyale") to setOf(Ownership.Rented), + ProviderId("hostuniversal") to setOf(Ownership.Rented), + ProviderId("iRegister") to setOf(Ownership.Rented), + ProviderId("M247") to setOf(Ownership.Rented), + ProviderId("Makonix") to setOf(Ownership.Rented), + ProviderId("PrivateLayer") to setOf(Ownership.Rented), + ProviderId("ptisp") to setOf(Ownership.Rented), + ProviderId("Qnax") to setOf(Ownership.Rented), + ProviderId("Quadranet") to setOf(Ownership.Rented), + ProviderId("techfutures") to setOf(Ownership.Rented), + ProviderId("Tzulo") to setOf(Ownership.Rented), + ProviderId("xtom") to setOf(Ownership.Rented), ) - private val DUMMY_SELECTED_PROVIDERS = - listOf( - Provider(ProviderId("31173"), Ownership.MullvadOwned), - Provider(ProviderId("100TB"), Ownership.Rented), - Provider(ProviderId("Blix"), Ownership.MullvadOwned), - Provider(ProviderId("Creanova"), Ownership.MullvadOwned), - Provider(ProviderId("DataPacket"), Ownership.Rented), - Provider(ProviderId("HostRoyale"), Ownership.Rented), - Provider(ProviderId("hostuniversal"), Ownership.Rented), - Provider(ProviderId("iRegister"), Ownership.Rented), - Provider(ProviderId("M247"), Ownership.Rented), - Provider(ProviderId("Makonix"), Ownership.Rented), - Provider(ProviderId("PrivateLayer"), Ownership.Rented), - Provider(ProviderId("ptisp"), Ownership.Rented), - Provider(ProviderId("Qnax"), Ownership.Rented), - Provider(ProviderId("Quadranet"), Ownership.Rented), - Provider(ProviderId("techfutures"), Ownership.Rented), - Provider(ProviderId("Tzulo"), Ownership.Rented), - Provider(ProviderId("xtom"), Ownership.Rented), - ) + private val DUMMY_SELECTED_PROVIDERS = DUMMY_RELAY_ALL_PROVIDERS.keys.toList() } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt index 13c237d6f4..e1b0c1a5d5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt @@ -31,6 +31,7 @@ internal fun CheckboxCell( modifier: Modifier = Modifier, title: String, checked: Boolean, + enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit, background: Color = MaterialTheme.colorScheme.surfaceContainerLow, startPadding: Dp = Dimens.mediumPadding, @@ -41,7 +42,7 @@ internal fun CheckboxCell( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .clickable { onCheckedChange(!checked) } + .clickable(enabled) { onCheckedChange(!checked) } .defaultMinSize(minHeight = minHeight) .fillMaxWidth() .background(background) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/FilterUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/FilterUiStatePreviewParameterProvider.kt index 8728143067..0aa3be8158 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/FilterUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/FilterUiStatePreviewParameterProvider.kt @@ -3,19 +3,17 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.RelayFilterUiState import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId -private val PROVIDER = - Provider(providerId = ProviderId("provider1"), ownership = Ownership.MullvadOwned) +private val PROVIDER_TO_OWNERSHIPS = mapOf(ProviderId("provider1") to setOf(Ownership.MullvadOwned)) class FilterUiStatePreviewParameterProvider : PreviewParameterProvider<RelayFilterUiState> { override val values = sequenceOf( RelayFilterUiState( + providerToOwnerships = PROVIDER_TO_OWNERSHIPS, selectedOwnership = Ownership.MullvadOwned, - allProviders = listOf(PROVIDER), - selectedProviders = listOf(PROVIDER), + selectedProviders = PROVIDER_TO_OWNERSHIPS.keys.toList(), ) ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt index 0af4199f40..20f69e3bcd 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt @@ -2,7 +2,6 @@ package net.mullvad.mullvadvpn.compose.preview import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.RelayItem @@ -56,7 +55,8 @@ private fun generateRelayItemRelay( RelayItem.Location.Relay( id = GeoLocationId.Hostname(city = cityCode, code = hostName), active = active, - provider = Provider(ProviderId("Provider"), Ownership.MullvadOwned), + provider = ProviderId("Provider"), + ownership = Ownership.MullvadOwned, daita = daita, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt index 6893397f9f..965999c7f1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt @@ -3,8 +3,6 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.RelayListType import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState -import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.usecase.FilterChip import net.mullvad.mullvadvpn.usecase.ModelOwnership diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt index 60788bffd5..3d3f937c73 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt @@ -46,7 +46,7 @@ import net.mullvad.mullvadvpn.compose.state.RelayFilterUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.viewmodel.FilterScreenSideEffect @@ -98,7 +98,7 @@ fun FilterScreen( onApplyClick: () -> Unit, onSelectedOwnership: (ownership: Ownership?) -> Unit, onAllProviderCheckChange: (isChecked: Boolean) -> Unit, - onSelectedProvider: (checked: Boolean, provider: Provider) -> Unit, + onSelectedProvider: (checked: Boolean, provider: ProviderId) -> Unit, ) { var providerExpanded by rememberSaveable { mutableStateOf(false) } var ownershipExpanded by rememberSaveable { mutableStateOf(false) } @@ -126,7 +126,7 @@ fun FilterScreen( itemsWithDivider( key = { it.name }, contentType = { ContentType.ITEM }, - items = state.filteredOwnershipByProviders, + items = state.selectableOwnerships, ) { ownership -> Ownership(ownership, state, onSelectedOwnership) } @@ -139,9 +139,17 @@ fun FilterScreen( AllProviders(state, onAllProviderCheckChange) } itemsWithDivider( - key = { it.providerId.value }, + key = { it.value }, contentType = { ContentType.ITEM }, - items = state.filteredProvidersByOwnership, + items = state.removedProviders, + ) { provider -> + RemovedProvider(provider, state, onSelectedProvider) + } + + itemsWithDivider( + key = { it.value }, + contentType = { ContentType.ITEM }, + items = state.selectableProviders, ) { provider -> Provider(provider, state, onSelectedProvider) } @@ -216,14 +224,30 @@ private fun LazyItemScope.AllProviders( @Composable private fun LazyItemScope.Provider( - provider: Provider, + providerId: ProviderId, + state: RelayFilterUiState, + onSelectedProvider: (checked: Boolean, providerId: ProviderId) -> Unit, +) { + CheckboxCell( + title = providerId.value, + checked = providerId in state.selectedProviders, + onCheckedChange = { checked -> onSelectedProvider(checked, providerId) }, + modifier = Modifier.animateItem(), + ) +} + +@Composable +private fun LazyItemScope.RemovedProvider( + providerId: ProviderId, state: RelayFilterUiState, - onSelectedProvider: (checked: Boolean, provider: Provider) -> Unit, + onSelectedProvider: (checked: Boolean, providerId: ProviderId) -> Unit, ) { + val checked = providerId in state.selectedProviders CheckboxCell( - title = provider.providerId.value, - checked = provider in state.selectedProviders, - onCheckedChange = { checked -> onSelectedProvider(checked, provider) }, + title = stringResource(R.string.removed_provider, providerId.value), + checked = checked, + enabled = checked, + onCheckedChange = { checked -> onSelectedProvider(checked, providerId) }, modifier = Modifier.animateItem(), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt index 01fe84f76c..2ddf35fad5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.Providers fun Ownership?.toOwnershipConstraint(): Constraint<Ownership> = @@ -11,18 +11,15 @@ fun Ownership?.toOwnershipConstraint(): Constraint<Ownership> = else -> Constraint.Only(this) } -fun Constraint<Providers>.toSelectedProviders(allProviders: List<Provider>): List<Provider> = +fun Constraint<Providers>.toSelectedProviders(allProviders: List<ProviderId>): List<ProviderId> = when (this) { Constraint.Any -> allProviders - is Constraint.Only -> - value.providers.toList().mapNotNull { provider -> - allProviders.firstOrNull { it.providerId == provider } - } + is Constraint.Only -> value.providers.toList() } -fun List<Provider>.toConstraintProviders(allProviders: List<Provider>): Constraint<Providers> = +fun List<ProviderId>.toConstraintProviders(allProviders: List<ProviderId>): Constraint<Providers> = if (size == allProviders.size) { Constraint.Any } else { - Constraint.Only(Providers(map { provider -> provider.providerId }.toHashSet())) + Constraint.Only(Providers(toHashSet())) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterUiState.kt index ffc26b0bb7..0c81ac0aa5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterUiState.kt @@ -1,26 +1,37 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId data class RelayFilterUiState( + private val providerToOwnerships: Map<ProviderId, Set<Ownership>> = emptyMap(), val selectedOwnership: Ownership? = null, - val allProviders: List<Provider> = emptyList(), - val selectedProviders: List<Provider> = allProviders, + val selectedProviders: List<ProviderId> = emptyList(), ) { - val isApplyButtonEnabled = selectedProviders.isNotEmpty() + val allProviders: List<ProviderId> = providerToOwnerships.keys.toList().sorted() - val filteredOwnershipByProviders = + val selectableOwnerships: List<Ownership> = if (selectedProviders.isEmpty()) { - Ownership.entries - } else { - Ownership.entries.filter { ownership -> - selectedProviders.any { provider -> provider.ownership == ownership } + Ownership.entries + } else { + providerToOwnerships + .filterKeys { it in selectedProviders } + .values + .flatten() + .distinct() } + .sorted() + + val selectableProviders: List<ProviderId> = + if (selectedOwnership != null) { + providerToOwnerships.filterValues { selectedOwnership in it }.keys.toList().sorted() + } else { + allProviders } - val filteredProvidersByOwnership = - if (selectedOwnership == null) allProviders - else allProviders.filter { provider -> provider.ownership == selectedOwnership } + + val removedProviders: List<ProviderId> = selectedProviders - allProviders + + val isApplyButtonEnabled = selectedProviders.isNotEmpty() val isAllProvidersChecked = allProviders.size == selectedProviders.size } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 650ee67eaa..4acf52c7b0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -37,7 +37,6 @@ import net.mullvad.mullvadvpn.ui.MainActivity import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.usecase.AccountExpiryInAppNotificationUseCase -import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase import net.mullvad.mullvadvpn.usecase.EmptyPaymentUseCase import net.mullvad.mullvadvpn.usecase.FilterChipUseCase import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase @@ -47,6 +46,7 @@ import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase import net.mullvad.mullvadvpn.usecase.PaymentUseCase import net.mullvad.mullvadvpn.usecase.PlayPaymentUseCase +import net.mullvad.mullvadvpn.usecase.ProviderToOwnershipsUseCase import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase @@ -156,7 +156,7 @@ val uiModule = module { single { SystemVpnSettingsAvailableUseCase(androidContext()) } single { CustomListActionUseCase(get(), get()) } single { SelectedLocationTitleUseCase(get(), get()) } - single { AvailableProvidersUseCase(get()) } + single { ProviderToOwnershipsUseCase(get()) } single { FilterCustomListsRelayItemUseCase(get(), get(), get(), get()) } single { CustomListsRelayItemUseCase(get(), get()) } single { CustomListRelayItemsUseCase(get(), get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt index 5584d8e991..21d3294de4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt @@ -35,7 +35,7 @@ private fun RelayItem.Location.hasOwnership(ownershipConstraint: Constraint<Owne when (this) { is RelayItem.Location.Country -> cities.any { it.hasOwnership(ownershipConstraint) } is RelayItem.Location.City -> relays.any { it.hasOwnership(ownershipConstraint) } - is RelayItem.Location.Relay -> this.provider.ownership == ownershipConstraint.value + is RelayItem.Location.Relay -> ownershipConstraint.value == ownership } } else { true @@ -46,8 +46,7 @@ private fun RelayItem.Location.hasProvider(providersConstraint: Constraint<Provi when (this) { is RelayItem.Location.Country -> cities.any { it.hasProvider(providersConstraint) } is RelayItem.Location.City -> relays.any { it.hasProvider(providersConstraint) } - is RelayItem.Location.Relay -> - providersConstraint.value.providers.contains(provider.providerId) + is RelayItem.Location.Relay -> provider in providersConstraint.value.providers } } else { true diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt index 37e5f71ecc..361e29944e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt @@ -3,10 +3,9 @@ package net.mullvad.mullvadvpn.usecase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import net.mullvad.mullvadvpn.compose.state.RelayListType -import net.mullvad.mullvadvpn.compose.state.toSelectedProviders import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.RelayListFilterRepository @@ -18,7 +17,7 @@ typealias ModelOwnership = Ownership class FilterChipUseCase( private val relayListFilterRepository: RelayListFilterRepository, - private val availableProvidersUseCase: AvailableProvidersUseCase, + private val providerToOwnershipsUseCase: ProviderToOwnershipsUseCase, private val settingsRepository: SettingsRepository, private val wireguardConstraintsRepository: WireguardConstraintsRepository, ) { @@ -26,19 +25,19 @@ class FilterChipUseCase( combine( relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, - availableProvidersUseCase(), + providerToOwnershipsUseCase(), settingsRepository.settingsUpdates, wireguardConstraintsRepository.wireguardConstraints, ) { selectedOwnership, selectedConstraintProviders, - allProviders, + providerOwnership, settings, wireguardConstraints -> filterChips( selectedOwnership = selectedOwnership, selectedConstraintProviders = selectedConstraintProviders, - allProviders = allProviders, + providerToOwnerships = providerOwnership, daitaDirectOnly = settings?.daitaAndDirectOnly() == true, isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true, relayListType = relayListType, @@ -48,7 +47,7 @@ class FilterChipUseCase( private fun filterChips( selectedOwnership: Constraint<Ownership>, selectedConstraintProviders: Constraint<Providers>, - allProviders: List<Provider>, + providerToOwnerships: Map<ProviderId, Set<Ownership>>, daitaDirectOnly: Boolean, isMultihopEnabled: Boolean, relayListType: RelayListType, @@ -58,10 +57,22 @@ class FilterChipUseCase( when (selectedConstraintProviders) { is Constraint.Any -> null is Constraint.Only -> - filterSelectedProvidersByOwnership( - selectedConstraintProviders.toSelectedProviders(allProviders), - ownershipFilter, - ) + selectedConstraintProviders.value.providers + .filter { providerId -> + if (ownershipFilter == null) { + true + } else { + val providerOwnerships = providerToOwnerships[providerId] + // If the provider has been removed from the relay list we add it + // so it is visible for the user, because we won't know what + // ownerships it had. + if (providerOwnerships == null) { + true + } else { + providerOwnerships.contains(ownershipFilter) + } + } + } .size } return buildList { @@ -83,13 +94,6 @@ class FilterChipUseCase( } } - private fun filterSelectedProvidersByOwnership( - selectedProviders: List<Provider>, - selectedOwnership: Ownership?, - ): List<Provider> = - if (selectedOwnership == null) selectedProviders - else selectedProviders.filter { it.ownership == selectedOwnership } - private fun Settings.daitaAndDirectOnly() = tunnelOptions.wireguard.daitaSettings.enabled && tunnelOptions.wireguard.daitaSettings.directOnly diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ProviderToOwnershipsUseCase.kt index 14aa3824cf..42f5a94e29 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ProviderToOwnershipsUseCase.kt @@ -2,18 +2,18 @@ package net.mullvad.mullvadvpn.usecase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.Ownership +import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.repository.RelayListRepository -class AvailableProvidersUseCase(private val relayListRepository: RelayListRepository) { - - operator fun invoke(): Flow<List<Provider>> = +class ProviderToOwnershipsUseCase(private val relayListRepository: RelayListRepository) { + operator fun invoke(): Flow<Map<ProviderId, Set<Ownership>>> = relayListRepository.relayList.map { relayList -> relayList .flatMap(RelayItem.Location.Country::cities) .flatMap(RelayItem.Location.City::relays) - .map(RelayItem.Location.Relay::provider) - .distinct() + .groupBy({ it.provider }, { it.ownership }) + .mapValues { (_, ownerships) -> ownerships.toSet() } } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt index 548d6b70a7..c05515126e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt @@ -16,27 +16,30 @@ import net.mullvad.mullvadvpn.compose.state.toConstraintProviders import net.mullvad.mullvadvpn.compose.state.toOwnershipConstraint import net.mullvad.mullvadvpn.compose.state.toSelectedProviders import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.repository.RelayListFilterRepository -import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase +import net.mullvad.mullvadvpn.usecase.ProviderToOwnershipsUseCase class FilterViewModel( - private val availableProvidersUseCase: AvailableProvidersUseCase, + private val providerToOwnershipsUseCase: ProviderToOwnershipsUseCase, private val relayListFilterRepository: RelayListFilterRepository, ) : ViewModel() { private val _uiSideEffect = Channel<FilterScreenSideEffect>() val uiSideEffect = _uiSideEffect.receiveAsFlow() private val selectedOwnership = MutableStateFlow<Ownership?>(null) - private val selectedProviders = MutableStateFlow<List<Provider>>(emptyList()) + private val selectedProviders = MutableStateFlow<List<ProviderId>>(emptyList()) init { viewModelScope.launch { selectedProviders.value = - combine(availableProvidersUseCase(), relayListFilterRepository.selectedProviders) { - allProviders, - selectedConstraintProviders -> - selectedConstraintProviders.toSelectedProviders(allProviders) + combine( + providerToOwnershipsUseCase(), + relayListFilterRepository.selectedProviders, + ) { providerToOwnerships, selectedConstraintProviders -> + selectedConstraintProviders.toSelectedProviders( + providerToOwnerships.keys.toList() + ) } .first() @@ -46,31 +49,25 @@ class FilterViewModel( } val uiState: StateFlow<RelayFilterUiState> = - combine(selectedOwnership, availableProvidersUseCase(), selectedProviders) { - selectedOwnership, - allProviders, - selectedProviders -> - RelayFilterUiState( - selectedOwnership = selectedOwnership, - allProviders = allProviders, - selectedProviders = selectedProviders, - ) - } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - RelayFilterUiState( - allProviders = emptyList(), - selectedOwnership = null, - selectedProviders = emptyList(), - ), - ) + combine(providerToOwnershipsUseCase(), selectedOwnership, selectedProviders, ::createState) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), RelayFilterUiState()) + + private fun createState( + providerToOwnerships: Map<ProviderId, Set<Ownership>>, + selectedOwnership: Ownership?, + selectedProviders: List<ProviderId>, + ): RelayFilterUiState = + RelayFilterUiState( + providerToOwnerships = providerToOwnerships, + selectedOwnership = selectedOwnership, + selectedProviders = selectedProviders, + ) fun setSelectedOwnership(ownership: Ownership?) { selectedOwnership.value = ownership } - fun setSelectedProvider(checked: Boolean, provider: Provider) { + fun setSelectedProvider(checked: Boolean, provider: ProviderId) { selectedProviders.value = if (checked) { selectedProviders.value + provider @@ -83,7 +80,7 @@ class FilterViewModel( viewModelScope.launch { selectedProviders.value = if (isChecked) { - availableProvidersUseCase().first() + providerToOwnershipsUseCase().first().keys.toList() } else { emptyList() } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt index 221d89cf40..bd1a759b77 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt @@ -9,7 +9,6 @@ import net.mullvad.mullvadvpn.compose.state.RelayListType import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.Settings @@ -23,13 +22,13 @@ import org.junit.jupiter.api.Test class FilterChipUseCaseTest { private val mockRelayListFilterRepository: RelayListFilterRepository = mockk() - private val mockAvailableProvidersUseCase: AvailableProvidersUseCase = mockk() + private val mockProviderToOwnershipsUseCase: ProviderToOwnershipsUseCase = mockk() private val mockSettingRepository: SettingsRepository = mockk() private val mockWireguardConstraintsRepository: WireguardConstraintsRepository = mockk() private val selectedOwnership = MutableStateFlow<Constraint<Ownership>>(Constraint.Any) private val selectedProviders = MutableStateFlow<Constraint<Providers>>(Constraint.Any) - private val availableProviders = MutableStateFlow<List<Provider>>(emptyList()) + private val providerToOwnerships = MutableStateFlow<Map<ProviderId, Set<Ownership>>>(emptyMap()) private val settings = MutableStateFlow<Settings>(mockk(relaxed = true)) private val wireguardConstraints = MutableStateFlow<WireguardConstraints>(mockk(relaxed = true)) @@ -39,7 +38,7 @@ class FilterChipUseCaseTest { fun setUp() { every { mockRelayListFilterRepository.selectedOwnership } returns selectedOwnership every { mockRelayListFilterRepository.selectedProviders } returns selectedProviders - every { mockAvailableProvidersUseCase() } returns availableProviders + every { mockProviderToOwnershipsUseCase() } returns providerToOwnerships every { mockSettingRepository.settingsUpdates } returns settings every { mockWireguardConstraintsRepository.wireguardConstraints } returns wireguardConstraints @@ -47,7 +46,7 @@ class FilterChipUseCaseTest { filterChipUseCase = FilterChipUseCase( relayListFilterRepository = mockRelayListFilterRepository, - availableProvidersUseCase = mockAvailableProvidersUseCase, + providerToOwnershipsUseCase = mockProviderToOwnershipsUseCase, settingsRepository = mockSettingRepository, wireguardConstraintsRepository = mockWireguardConstraintsRepository, ) @@ -74,10 +73,10 @@ class FilterChipUseCaseTest { // Arrange val expectedProviders = Providers(providers = setOf(ProviderId("1"), ProviderId("2"))) selectedProviders.value = Constraint.Only(expectedProviders) - availableProviders.value = - listOf( - Provider(ProviderId("1"), Ownership.MullvadOwned), - Provider(ProviderId("2"), Ownership.Rented), + providerToOwnerships.value = + mapOf( + ProviderId("1") to setOf(Ownership.MullvadOwned), + ProviderId("2") to setOf(Ownership.Rented), ) filterChipUseCase(RelayListType.EXIT).test { @@ -93,10 +92,10 @@ class FilterChipUseCaseTest { val expectedOwnership = Ownership.MullvadOwned selectedProviders.value = Constraint.Only(expectedProviders) selectedOwnership.value = Constraint.Only(expectedOwnership) - availableProviders.value = - listOf( - Provider(ProviderId("1"), Ownership.MullvadOwned), - Provider(ProviderId("2"), Ownership.Rented), + providerToOwnerships.value = + mapOf( + ProviderId("1") to setOf(Ownership.MullvadOwned), + ProviderId("2") to setOf(Ownership.Rented), ) filterChipUseCase(RelayListType.EXIT).test { @@ -185,4 +184,27 @@ class FilterChipUseCaseTest { filterChipUseCase(RelayListType.EXIT).test { assertLists(emptyList(), awaitItem()) } } + + @Test + fun `ensure that a selected provider that is not in the provider list is still counted`() = + runTest { + // Arrange + val expectedProviders = Providers(providers = setOf(ProviderId("1"))) + val expectedOwnership = Ownership.MullvadOwned + selectedProviders.value = Constraint.Only(expectedProviders) + selectedOwnership.value = Constraint.Only(expectedOwnership) + providerToOwnerships.value = + mapOf( + ProviderId("2") to setOf(Ownership.MullvadOwned), + ProviderId("3") to setOf(Ownership.Rented), + ) + + // Act, Assert + filterChipUseCase(RelayListType.EXIT).test { + assertLists( + listOf(FilterChip.Ownership(expectedOwnership), FilterChip.Provider(1)), + awaitItem(), + ) + } + } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt index 75b8deca8f..865b4ce471 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt @@ -22,7 +22,6 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.relaylist.descendants @@ -357,11 +356,8 @@ class CustomListLocationsViewModelTest { "gbg-1", ), active = true, - provider = - Provider( - ProviderId("Provider"), - ownership = Ownership.MullvadOwned, - ), + provider = ProviderId("Provider"), + ownership = Ownership.MullvadOwned, daita = false, ) ), @@ -377,7 +373,8 @@ class CustomListLocationsViewModelTest { "cph-1", ), active = true, - provider = Provider(ProviderId("Provider"), ownership = Ownership.MullvadOwned), + provider = ProviderId("Provider"), + ownership = Ownership.MullvadOwned, daita = false, ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt index 4453f08ee4..49b9b4baff 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt @@ -19,11 +19,10 @@ import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership -import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.repository.RelayListFilterRepository -import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase +import net.mullvad.mullvadvpn.usecase.ProviderToOwnershipsUseCase import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -31,49 +30,43 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(TestCoroutineRule::class) class FilterViewModelTest { - private val mockAvailableProvidersUseCase: AvailableProvidersUseCase = mockk(relaxed = true) + private val mockProvidersOwnershipUseCase: ProviderToOwnershipsUseCase = mockk(relaxed = true) private val mockRelayListFilterRepository: RelayListFilterRepository = mockk() private lateinit var viewModel: FilterViewModel private val selectedOwnership = MutableStateFlow<Constraint<Ownership>>(Constraint.Only(Ownership.MullvadOwned)) private val dummyListOfAllProviders = - listOf( - Provider(ProviderId("31173"), Ownership.MullvadOwned), - Provider(ProviderId("100TB"), Ownership.Rented), - Provider(ProviderId("Blix"), Ownership.MullvadOwned), - Provider(ProviderId("Creanova"), Ownership.MullvadOwned), - Provider(ProviderId("DataPacket"), Ownership.Rented), - Provider(ProviderId("HostRoyale"), Ownership.Rented), - Provider(ProviderId("hostuniversal"), Ownership.Rented), - Provider(ProviderId("iRegister"), Ownership.Rented), - Provider(ProviderId("M247"), Ownership.Rented), - Provider(ProviderId("Makonix"), Ownership.Rented), - Provider(ProviderId("PrivateLayer"), Ownership.Rented), - Provider(ProviderId("ptisp"), Ownership.Rented), - Provider(ProviderId("Qnax"), Ownership.Rented), - Provider(ProviderId("Quadranet"), Ownership.Rented), - Provider(ProviderId("techfutures"), Ownership.Rented), - Provider(ProviderId("Tzulo"), Ownership.Rented), - Provider(ProviderId("xtom"), Ownership.Rented), - ) - private val mockSelectedProviders: List<Provider> = - listOf( - Provider(ProviderId("31173"), Ownership.MullvadOwned), - Provider(ProviderId("Blix"), Ownership.MullvadOwned), - Provider(ProviderId("Creanova"), Ownership.MullvadOwned), + mapOf( + ProviderId("31173") to setOf(Ownership.MullvadOwned), + ProviderId("100TB") to setOf(Ownership.Rented), + ProviderId("Blix") to setOf(Ownership.MullvadOwned), + ProviderId("Creanova") to setOf(Ownership.MullvadOwned), + ProviderId("DataPacket") to setOf(Ownership.Rented, Ownership.MullvadOwned), + ProviderId("HostRoyale") to setOf(Ownership.Rented), + ProviderId("hostuniversal") to setOf(Ownership.Rented), + ProviderId("iRegister") to setOf(Ownership.Rented), + ProviderId("M247") to setOf(Ownership.Rented), + ProviderId("Makonix") to setOf(Ownership.Rented), + ProviderId("PrivateLayer") to setOf(Ownership.Rented), + ProviderId("ptisp") to setOf(Ownership.Rented), + ProviderId("Qnax") to setOf(Ownership.Rented), + ProviderId("Quadranet") to setOf(Ownership.Rented), + ProviderId("techfutures") to setOf(Ownership.Rented), + ProviderId("Tzulo") to setOf(Ownership.Rented), + ProviderId("xtom") to setOf(Ownership.Rented), ) + private val mockSelectedProviders: List<ProviderId> = + listOf(ProviderId("31173"), ProviderId("Blix"), ProviderId("Creanova")) @BeforeEach fun setup() { every { mockRelayListFilterRepository.selectedOwnership } returns selectedOwnership - every { mockAvailableProvidersUseCase() } returns flowOf(dummyListOfAllProviders) + every { mockProvidersOwnershipUseCase() } returns flowOf(dummyListOfAllProviders) every { mockRelayListFilterRepository.selectedProviders } returns - MutableStateFlow( - Constraint.Only(Providers(mockSelectedProviders.map { it.providerId }.toSet())) - ) + MutableStateFlow(Constraint.Only(Providers(mockSelectedProviders.toHashSet()))) viewModel = FilterViewModel( - availableProvidersUseCase = mockAvailableProvidersUseCase, + providerToOwnershipsUseCase = mockProvidersOwnershipUseCase, relayListFilterRepository = mockRelayListFilterRepository, ) } @@ -101,7 +94,7 @@ class FilterViewModelTest { fun `setSelectionProvider should emit uiState where selectedProviders include the selected provider`() = runTest { // Arrange - val mockSelectedProvidersList = Provider(ProviderId("ptisp"), Ownership.Rented) + val mockSelectedProvidersList = ProviderId("ptisp") // Assert viewModel.uiState.test { assertLists(awaitItem().selectedProviders, mockSelectedProviders) @@ -117,7 +110,7 @@ class FilterViewModelTest { fun `setAllProvider with true should emit uiState with selectedProviders includes all providers`() = runTest { // Arrange - val mockProvidersList = dummyListOfAllProviders + val mockProvidersList = dummyListOfAllProviders.keys.toList() // Act viewModel.setAllProviders(true) // Assert @@ -133,7 +126,7 @@ class FilterViewModelTest { // Arrange val mockOwnership = Ownership.MullvadOwned.toOwnershipConstraint() val mockSelectedProviders = - mockSelectedProviders.toConstraintProviders(dummyListOfAllProviders) + mockSelectedProviders.toConstraintProviders(dummyListOfAllProviders.keys.toList()) coEvery { mockRelayListFilterRepository.updateSelectedOwnershipAndProviderFilter( mockOwnership, @@ -152,4 +145,26 @@ class FilterViewModelTest { ) } } + + @Test + fun `ensure that providers with multiple ownership are only returned once`() = runTest { + // Arrange + val expectedProviderList = dummyListOfAllProviders.keys.toList() + + // Assert + viewModel.uiState.test { + val state = awaitItem() + assertLists(expectedProviderList, state.allProviders) + } + } + + @Test + fun `ensure that providers are sorted by name`() = runTest { + // Assert + viewModel.uiState.test { + val state = awaitItem() + assertEquals(state.allProviders.sorted(), state.allProviders) + assertEquals(state.selectableProviders.sorted(), state.selectableProviders) + } + } } |
