Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server ip overrides feature #5969

Merged
merged 7 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Line wrap the file at 100 chars. Th
- Add auto connect and lockdown mode guide on platforms that has system vpn settings.
- Add 3D map to Connect screen.
- Add the ability to create and manage custom lists of relays.
- Add Server IP overrides feature.

### Changed
- Change default obfuscation setting to `auto`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package net.mullvad.mullvadvpn.compose.dialog

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.test.RESET_SERVER_IP_OVERRIDE_CANCEL_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.RESET_SERVER_IP_OVERRIDE_RESET_TEST_TAG
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

class ResetServerIPOverridesConfirmationDialogTest {
@OptIn(ExperimentalTestApi::class)
@JvmField
@RegisterExtension
val composeExtension = createEdgeToEdgeComposeExtension()

@BeforeEach
fun setup() {
MockKAnnotations.init(this)
}

@Test
fun ensure_cancel_click_works() =
composeExtension.use {
val clickHandler: () -> Unit = mockk(relaxed = true)

// Arrange
setContentWithTheme {
ResetServerIpOverridesConfirmationDialog(
onNavigateBack = clickHandler,
onClearAllOverrides = {}
)
}

// Act
onNodeWithTag(RESET_SERVER_IP_OVERRIDE_CANCEL_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}

@Test
fun ensure_reset_click_works() =
composeExtension.use {
val clickHandler: () -> Unit = mockk(relaxed = true)

// Arrange
setContentWithTheme {
ResetServerIpOverridesConfirmationDialog(
onNavigateBack = {},
onClearAllOverrides = clickHandler
)
}

// Act
onNodeWithTag(RESET_SERVER_IP_OVERRIDE_RESET_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package net.mullvad.mullvadvpn.compose.screen

import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_IMPORT_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_INFO_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewState
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

@ExperimentalTestApi
class ServerIpOverridesScreenTest {
@JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension()

@BeforeEach
fun setup() {
MockKAnnotations.init(this)
}

@Suppress("TestFunctionName")
@Composable
private fun ScreenWithDefault(
state: ServerIpOverridesViewState,
onBackClick: () -> Unit = {},
onInfoClick: () -> Unit = {},
onResetOverridesClick: () -> Unit = {},
onImportByFile: () -> Unit = {},
onImportByText: () -> Unit = {},
) {
ServerIpOverridesScreen(
state = state,
onBackClick = onBackClick,
onInfoClick = onInfoClick,
onResetOverridesClick = onResetOverridesClick,
onImportByFile = onImportByFile,
onImportByText = onImportByText
)
}

@Test
fun ensure_overrides_inactive_is_displayed() =
composeExtension.use {
// Arrange
setContentWithTheme {
ScreenWithDefault(state = ServerIpOverridesViewState.Loaded(false))
}

// Assert
onNodeWithText("Overrides inactive").assertExists()
}

@Test
fun ensure_overrides_active_is_displayed() =
composeExtension.use {
// Arrange
setContentWithTheme {
ScreenWithDefault(state = ServerIpOverridesViewState.Loaded(true))
}

// Assert
onNodeWithText("Overrides active").assertExists()
}

@Test
fun ensure_overrides_active_shows_warning_on_import() =
composeExtension.use {
// Arrange
setContentWithTheme {
ScreenWithDefault(state = ServerIpOverridesViewState.Loaded(true))
}

// Act
onNodeWithTag(testTag = SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick()

// Assert
onNodeWithText(
"Importing new overrides might replace some previously imported overrides."
)
.assertExists()
}

@Test
fun ensure_info_click_works() =
composeExtension.use {
// Arrange
val clickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
ScreenWithDefault(
state = ServerIpOverridesViewState.Loaded(false),
onInfoClick = clickHandler
)
}

// Act
onNodeWithTag(SERVER_IP_OVERRIDE_INFO_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}

@Test
fun ensure_reset_click_works() =
composeExtension.use {
// Arrange
val clickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
ScreenWithDefault(
state = ServerIpOverridesViewState.Loaded(true),
onResetOverridesClick = clickHandler
)
}

// Act
onNodeWithTag(SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG).performClick()
onNodeWithTag(SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}

@Test
fun ensure_import_by_file_works() =
composeExtension.use {
// Arrange
val clickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
ScreenWithDefault(
state = ServerIpOverridesViewState.Loaded(false),
onImportByFile = clickHandler
)
}

// Act
onNodeWithTag(SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick()
onNodeWithTag(SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}

@Test
fun ensure_import_by_text() =
composeExtension.use {
// Arrange
val clickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
ScreenWithDefault(
state = ServerIpOverridesViewState.Loaded(false),
onImportByText = clickHandler
)
}

// Act
onNodeWithTag(SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick()
onNodeWithTag(SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG).performClick()

// Assert
verify { clickHandler() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.mullvad.mullvadvpn.compose.button

import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import net.mullvad.mullvadvpn.R

@Composable
fun InfoIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
contentDescription: String? = null,
iconTint: Color = MaterialTheme.colorScheme.onPrimary
) {
IconButton(modifier = modifier, onClick = onClick) {
Icon(
painter = painterResource(id = R.drawable.icon_info),
contentDescription = contentDescription,
tint = iconTint
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ private fun PreviewIconCell() {
@Composable
fun IconCell(
iconId: Int?,
contentDescription: String? = null,
title: String,
modifier: Modifier = Modifier,
contentDescription: String? = null,
titleStyle: TextStyle = MaterialTheme.typography.labelLarge,
titleColor: Color = MaterialTheme.colorScheme.onPrimary,
onClick: () -> Unit = {},
background: Color = MaterialTheme.colorScheme.primary,
enabled: Boolean = true,
enabled: Boolean = true
) {
BaseCell(
headlineContent = {
Expand All @@ -49,6 +50,7 @@ fun IconCell(
},
onCellClicked = onClick,
background = background,
isRowEnabled = enabled
isRowEnabled = enabled,
modifier = modifier
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package net.mullvad.mullvadvpn.compose.cell

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorSmall
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.lib.theme.color.selected

@Preview
@Composable
private fun PreviewServerIpOverridesCell() {
AppTheme { ServerIpOverridesCell(active = true) }
}

@Composable
fun ServerIpOverridesCell(
active: Boolean?,
modifier: Modifier = Modifier,
activeColor: Color = MaterialTheme.colorScheme.selected,
inactiveColor: Color = MaterialTheme.colorScheme.error,
) {
BaseCell(
modifier = modifier,
iconView = {
if (active == null) {
MullvadCircularProgressIndicatorSmall()
} else {
Box(
modifier =
Modifier.size(Dimens.relayCircleSize)
.background(
color =
when {
active -> activeColor
else -> inactiveColor
},
shape = CircleShape
)
)
}
},
headlineContent = {
if (active != null) {
Text(
text =
if (active) stringResource(id = R.string.server_ip_overrides_active)
else stringResource(id = R.string.server_ip_overrides_inactive),
color = MaterialTheme.colorScheme.onPrimary,
modifier =
Modifier.weight(1f)
.alpha(
if (active) {
AlphaVisible
} else {
AlphaInactive
}
)
.padding(
horizontal = Dimens.smallPadding,
vertical = Dimens.mediumPadding
)
)
}
},
isRowEnabled = false
)
}
Loading
Loading