summaryrefslogtreecommitdiffhomepage
path: root/android/app/src/test
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-04-15 15:59:12 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2025-04-22 12:12:12 +0200
commit926468949b2facb49182c1a5a7597b720f9e5cc7 (patch)
tree30950262e3f6adb784a12d89f8c8def37a604356 /android/app/src/test
parent3a58848059a1fb8c429a49acff9e2f57b1d91fd2 (diff)
downloadmullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.tar.xz
mullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.zip
Implement manage devices screen
Diffstat (limited to 'android/app/src/test')
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt96
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt147
2 files changed, 243 insertions, 0 deletions
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt
new file mode 100644
index 0000000000..90024be351
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt
@@ -0,0 +1,96 @@
+package net.mullvad.mullvadvpn.utils
+
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import kotlin.test.assertEquals
+import net.mullvad.mullvadvpn.util.appendTextWithStyledSubstring
+import org.junit.jupiter.api.Test
+
+class AppendTextWithStyledSubstringTest {
+ @Test
+ fun `empty input should result in empty output`() {
+
+ val output = buildAnnotatedString {
+ appendTextWithStyledSubstring(
+ text = "",
+ substring = "abc",
+ substringStyle = SpanStyle(),
+ )
+ }
+
+ assertEquals("", output.text)
+ }
+
+ @Test
+ fun `split only should result in split only`() {
+
+ val split = "abc"
+
+ val output = buildAnnotatedString {
+ appendTextWithStyledSubstring(
+ text = split,
+ substring = split,
+ substringStyle = SpanStyle(),
+ )
+ }
+
+ assertEquals(split, output.text)
+ }
+
+ @Test
+ fun `split twice should result in split twice`() {
+
+ val split = "abcabc"
+
+ val output = buildAnnotatedString {
+ appendTextWithStyledSubstring(
+ text = split,
+ substring = split,
+ substringStyle = SpanStyle(),
+ )
+ }
+
+ assertEquals(split, output.text)
+ }
+
+ @Test
+ fun `split anywhere should return the input text`() {
+
+ val text = "abca longer abc string to split abc"
+ val split = "abc"
+
+ val output = buildAnnotatedString {
+ appendTextWithStyledSubstring(
+ text = text,
+ substring = split,
+ substringStyle = SpanStyle(),
+ )
+ }
+
+ assertEquals(text, output.text)
+ }
+
+ @Test
+ fun `span styles should be applied to all matching substrings`() {
+
+ val text = "Cool Cat: your username is Cool Cat."
+ val split = "Cool Cat"
+
+ val output = buildAnnotatedString {
+ appendTextWithStyledSubstring(
+ text = text,
+ substring = split,
+ substringStyle = SpanStyle(),
+ )
+ }
+
+ assertEquals(text, output.text)
+ assertEquals(2, output.spanStyles.size)
+
+ assertEquals(0, output.spanStyles[0].start)
+ assertEquals(8, output.spanStyles[0].end)
+
+ assertEquals(27, output.spanStyles[1].start)
+ assertEquals(35, output.spanStyles[1].end)
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt
new file mode 100644
index 0000000000..ac5446cc07
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt
@@ -0,0 +1,147 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
+import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import java.time.ZonedDateTime
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.screen.DeviceListNavArgs
+import net.mullvad.mullvadvpn.compose.state.ManageDevicesUiState
+import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.AccountNumber
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.util.Lce
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExperimentalCoroutinesApi
+@ExtendWith(TestCoroutineRule::class)
+class ManageDevicesViewModelTest {
+
+ private val mockDeviceRepository: DeviceRepository = mockk()
+ private val mockSavedStateHandle: SavedStateHandle = mockk(relaxed = true)
+
+ private lateinit var viewModel: ManageDevicesViewModel
+
+ @BeforeEach
+ fun setup() {
+ // Mock SavedStateHandle to return the account number
+ every { mockSavedStateHandle.get<AccountNumber>("accountNumber") } returns testAccountNumber
+
+ // Mock successful device list fetch by default
+ coEvery { mockDeviceRepository.deviceList(testAccountNumber) } returns
+ testDeviceList.right()
+
+ every { mockDeviceRepository.deviceState } returns MutableStateFlow(deviceState)
+
+ viewModel =
+ ManageDevicesViewModel(
+ deviceRepository = mockDeviceRepository,
+ deviceListViewModel =
+ DeviceListViewModel(
+ deviceRepository = mockDeviceRepository,
+ dispatcher = UnconfinedTestDispatcher(),
+ savedStateHandle =
+ DeviceListNavArgs(accountNumber = testAccountNumber)
+ .toSavedStateHandle(),
+ ),
+ )
+ }
+
+ @AfterEach
+ fun tearDown() {
+ viewModel.viewModelScope.coroutineContext.cancel()
+ unmockkAll()
+ }
+
+ @Test
+ fun `initial state should be Loading followed by Content`() = runTest {
+ // Initial state is Loading
+ assertIs<Lce.Loading>(viewModel.uiState.value)
+
+ viewModel.uiState.test {
+ val contentState = awaitItem()
+ assertIs<Lce.Content<ManageDevicesUiState>>(contentState)
+ assertEquals(3, contentState.value.devices.size)
+ }
+ }
+
+ @Test
+ fun `fetchDevices should update state to Error on failure`() = runTest {
+ val error = GetDeviceListError.Unknown(RuntimeException("Network failed"))
+ coEvery { mockDeviceRepository.deviceList(testAccountNumber) } returns error.left()
+
+ viewModel.uiState.test {
+ val errorState = awaitItem()
+ assertIs<Lce.Error<GetDeviceListError>>(errorState)
+ assertEquals(error, errorState.error)
+ }
+ }
+
+ @Test
+ fun `the logged in device should appear first in the list`() = runTest {
+ viewModel.uiState.test {
+ val contentState = awaitItem()
+ assertIs<Lce.Content<ManageDevicesUiState>>(contentState)
+
+ val devices = contentState.value.devices
+ assertEquals(testDeviceId2, devices[0].device.id)
+ assertTrue(devices[0].isCurrentDevice)
+ assertFalse(devices[1].isCurrentDevice)
+ assertFalse(devices[2].isCurrentDevice)
+ }
+ }
+
+ companion object {
+ private val testAccountNumber = AccountNumber("1234567890123456")
+ private val testDeviceId1 = DeviceId.fromString("12345678-1234-5678-1234-567812345678")
+ private val testDeviceId2 = DeviceId.fromString("87654321-1234-5678-1234-567812345678")
+ private val testDeviceId3 = DeviceId.fromString("87654321-4321-5678-1234-567812345678")
+
+ private val testDevice1 =
+ Device(
+ id = testDeviceId1,
+ name = "Device 1",
+ creationDate = ZonedDateTime.now().minusSeconds(100),
+ )
+
+ private val testDevice2 =
+ Device(
+ id = testDeviceId2,
+ name = "Device 2",
+ creationDate = ZonedDateTime.now().minusSeconds(200),
+ )
+
+ private val testDevice3 =
+ Device(
+ id = testDeviceId3,
+ name = "Device 3",
+ creationDate = ZonedDateTime.now().minusSeconds(300),
+ )
+ private val testDeviceList = listOf(testDevice1, testDevice2, testDevice3)
+
+ private val deviceState =
+ DeviceState.LoggedIn(accountNumber = testAccountNumber, device = testDevice2)
+ }
+}