diff options
| author | David Göransson <david.goransson90@gmail.com> | 2023-10-13 12:21:10 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson90@gmail.com> | 2023-10-23 11:28:23 +0200 |
| commit | 02b7b4313323fcb1bb10f72ccb956177d44ecf16 (patch) | |
| tree | 0c66180f1b208a00ecb9885d0d9f4372d578660e /android/app | |
| parent | c085b31acdc002076106a30f7cd1dcdcd43daf05 (diff) | |
| download | mullvadvpn-02b7b4313323fcb1bb10f72ccb956177d44ecf16.tar.xz mullvadvpn-02b7b4313323fcb1bb10f72ccb956177d44ecf16.zip | |
Add tests
Diffstat (limited to 'android/app')
8 files changed, 514 insertions, 114 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt index d6ef5d3311..56894addea 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt @@ -13,18 +13,18 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.compose.state.ConnectNotificationState import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_TEST_TAG -import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER +import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.relaylist.RelayItem +import net.mullvad.mullvadvpn.repository.InAppNotification import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.talpid.net.TransportProtocol @@ -86,8 +86,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationBlocked + inAppNotification = InAppNotification.TunnelStateBlocked ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -123,8 +122,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationBlocked + inAppNotification = InAppNotification.TunnelStateBlocked ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -158,7 +156,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -191,7 +189,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -225,7 +223,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -259,7 +257,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -295,8 +293,8 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationError( + inAppNotification = + InAppNotification.TunnelStateError( ErrorState(ErrorStateCause.StartTunnelError, true) ) ), @@ -335,8 +333,8 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationError( + inAppNotification = + InAppNotification.TunnelStateError( ErrorState(ErrorStateCause.StartTunnelError, false) ) ), @@ -372,8 +370,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationBlocked + inAppNotification = InAppNotification.TunnelStateBlocked ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -409,8 +406,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationBlocked + inAppNotification = InAppNotification.TunnelStateBlocked ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -446,7 +442,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onSwitchLocationClick = mockedClickHandler @@ -479,7 +475,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onDisconnectClick = mockedClickHandler @@ -512,7 +508,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onReconnectClick = mockedClickHandler @@ -544,7 +540,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onConnectClick = mockedClickHandler @@ -576,7 +572,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onCancelClick = mockedClickHandler @@ -609,7 +605,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), onToggleTunnelInfo = mockedClickHandler @@ -649,7 +645,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = true, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = ConnectNotificationState.HideNotification + inAppNotification = null ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -688,8 +684,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowVersionInfoNotification(versionInfo) + inAppNotification = InAppNotification.UpdateAvailable(versionInfo) ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -726,8 +721,7 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowVersionInfoNotification(versionInfo) + inAppNotification = InAppNotification.UnsupportedVersion(versionInfo) ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -759,10 +753,9 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, - deviceName = null, + deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowAccountExpiryNotification(expiryDate) + inAppNotification = InAppNotification.AccountExpiry(expiryDate) ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) @@ -801,15 +794,14 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowVersionInfoNotification(versionInfo) + inAppNotification = InAppNotification.UnsupportedVersion(versionInfo) ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) } // Act - composeTestRule.onNodeWithTag(NOTIFICATION_BANNER).performClick() + composeTestRule.onNodeWithTag(NOTIFICATION_BANNER_ACTION).performClick() // Assert verify { mockedClickHandler.invoke() } @@ -835,15 +827,14 @@ class ConnectScreenTest { isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, - connectNotificationState = - ConnectNotificationState.ShowAccountExpiryNotification(expiryDate) + inAppNotification = InAppNotification.AccountExpiry(expiryDate) ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() ) } // Act - composeTestRule.onNodeWithTag(NOTIFICATION_BANNER).performClick() + composeTestRule.onNodeWithTag(NOTIFICATION_BANNER_ACTION).performClick() // Assert verify { mockedClickHandler.invoke() } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt new file mode 100644 index 0000000000..30b54cea11 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt @@ -0,0 +1,102 @@ +package net.mullvad.mullvadvpn + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.repository.InAppNotification +import net.mullvad.mullvadvpn.repository.InAppNotificationController +import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase +import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase +import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase +import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase +import net.mullvad.talpid.tunnel.ErrorState +import org.joda.time.DateTime +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class InAppNotificationControllerTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + private lateinit var inAppNotificationController: InAppNotificationController + private val accountExpiryNotifications = MutableStateFlow(emptyList<InAppNotification>()) + private val newDeviceNotifications = MutableStateFlow(emptyList<InAppNotification.NewDevice>()) + private val versionNotifications = MutableStateFlow(emptyList<InAppNotification>()) + private val tunnelStateNotifications = MutableStateFlow(emptyList<InAppNotification>()) + + private lateinit var job: Job + + @Before + fun setup() { + MockKAnnotations.init(this) + + val accountExpiryNotificationUseCase: AccountExpiryNotificationUseCase = mockk() + val newDeviceNotificationUseCase: NewDeviceNotificationUseCase = mockk() + val versionNotificationUseCase: VersionNotificationUseCase = mockk() + val tunnelStateNotificationUseCase: TunnelStateNotificationUseCase = mockk() + every { accountExpiryNotificationUseCase.notifications() } returns + accountExpiryNotifications + every { newDeviceNotificationUseCase.notifications() } returns newDeviceNotifications + every { versionNotificationUseCase.notifications() } returns versionNotifications + every { tunnelStateNotificationUseCase.notifications() } returns tunnelStateNotifications + job = Job() + + inAppNotificationController = + InAppNotificationController( + accountExpiryNotificationUseCase, + newDeviceNotificationUseCase, + versionNotificationUseCase, + tunnelStateNotificationUseCase, + CoroutineScope(job + testCoroutineRule.testDispatcher) + ) + } + + @After + fun teardown() { + job.cancel() + unmockkAll() + } + + @Test + fun `ensure all notifications have the right priority`() = runTest { + val newDevice = InAppNotification.NewDevice("") + newDeviceNotifications.value = listOf(newDevice) + + val errorState: ErrorState = mockk() + val tunnelStateBlocked = InAppNotification.TunnelStateBlocked + val tunnelStateError = InAppNotification.TunnelStateError(errorState) + tunnelStateNotifications.value = listOf(tunnelStateBlocked, tunnelStateError) + + val unsupportedVersion = InAppNotification.UnsupportedVersion(mockk()) + val updateAvailable = InAppNotification.UpdateAvailable(mockk()) + versionNotifications.value = listOf(unsupportedVersion, updateAvailable) + + val accountExpiry = InAppNotification.AccountExpiry(DateTime.now()) + accountExpiryNotifications.value = listOf(accountExpiry) + + inAppNotificationController.notifications.test { + val notifications = awaitItem() + + assertEquals( + listOf( + tunnelStateError, + tunnelStateBlocked, + unsupportedVersion, + accountExpiry, + newDevice, + updateAvailable, + ), + notifications + ) + } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt new file mode 100644 index 0000000000..5341708d3b --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt @@ -0,0 +1,75 @@ +package net.mullvad.mullvadvpn.usecase + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.InAppNotification +import org.joda.time.DateTime +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AccountExpiryNotificationUseCaseTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + private val accountExpiry = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing) + private lateinit var accountExpiryNotificationUseCase: AccountExpiryNotificationUseCase + + @Before + fun setup() { + MockKAnnotations.init(this) + + val accountRepository = mockk<AccountRepository>() + every { accountRepository.accountExpiryState } returns accountExpiry + + accountExpiryNotificationUseCase = AccountExpiryNotificationUseCase(accountRepository) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `ensure notifications are empty by default`() = runTest { + // Arrange, Act, Assert + accountExpiryNotificationUseCase.notifications().test { + assertTrue { awaitItem().isEmpty() } + } + } + + @Test + fun `ensure account expiry within 3 days generates notification`() = runTest { + // Arrange, Act, Assert + accountExpiryNotificationUseCase.notifications().test { + assertTrue { awaitItem().isEmpty() } + val closeToExpiry = AccountExpiry.Available(DateTime.now().plusDays(2)) + accountExpiry.value = closeToExpiry + + assertEquals( + listOf(InAppNotification.AccountExpiry(closeToExpiry.expiryDateTime)), + awaitItem() + ) + } + } + + @Test + fun `ensure an expire of 4 days in the future does not produce a notification`() = runTest { + // Arrange, Act, Assert + accountExpiryNotificationUseCase.notifications().test { + assertTrue { awaitItem().isEmpty() } + accountExpiry.value = AccountExpiry.Available(DateTime.now().plusDays(4)) + expectNoEvents() + } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt new file mode 100644 index 0000000000..bd375d729a --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt @@ -0,0 +1,81 @@ +package net.mullvad.mullvadvpn.usecase + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.model.AccountAndDevice +import net.mullvad.mullvadvpn.model.Device +import net.mullvad.mullvadvpn.model.DeviceState +import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.repository.InAppNotification +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class NewDeviceUseNotificationCaseTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + private val deviceName = "Frank Zebra" + private val deviceState = + MutableStateFlow<DeviceState>( + DeviceState.LoggedIn( + accountAndDevice = AccountAndDevice("", Device("", deviceName, byteArrayOf(), "")) + ) + ) + private lateinit var newDeviceNotificationUseCase: NewDeviceNotificationUseCase + + @Before + fun setup() { + MockKAnnotations.init(this) + + val mockDeviceRepository: DeviceRepository = mockk() + every { mockDeviceRepository.deviceState } returns deviceState + newDeviceNotificationUseCase = + NewDeviceNotificationUseCase(deviceRepository = mockDeviceRepository) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `ensure empty by default`() = runTest { + // Arrange, Act, Assert + newDeviceNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } } + } + + @Test + fun `ensure NewDevice notification is created and contains device name`() = runTest { + newDeviceNotificationUseCase.notifications().test { + // Arrange, Act + awaitItem() + newDeviceNotificationUseCase.newDeviceCreated() + + // Assert + assertEquals(awaitItem(), listOf(InAppNotification.NewDevice(deviceName))) + } + } + + @Test + fun `ensure NewDevice notification is cleared`() = runTest { + newDeviceNotificationUseCase.notifications().test { + // Arrange, Act + awaitItem() + newDeviceNotificationUseCase.newDeviceCreated() + awaitItem() + newDeviceNotificationUseCase.clearNewDeviceCreatedNotification() + + // Assert + assertEquals(awaitItem(), emptyList()) + } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt new file mode 100644 index 0000000000..1b89c92be7 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt @@ -0,0 +1,94 @@ +package net.mullvad.mullvadvpn.usecase + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.repository.InAppNotification +import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState +import net.mullvad.talpid.tunnel.ActionAfterDisconnect +import net.mullvad.talpid.tunnel.ErrorState +import net.mullvad.talpid.util.EventNotifier +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class TunnelStateNotificationUseCaseTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + private val mockServiceConnectionManager: ServiceConnectionManager = mockk() + private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() + private val mockConnectionProxy: ConnectionProxy = mockk() + + private val serviceConnectionState = + MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected) + private lateinit var tunnelStateNotificationUseCase: TunnelStateNotificationUseCase + + private val eventNotifierTunnelUiState = EventNotifier<TunnelState>(TunnelState.Disconnected) + + @Before + fun setup() { + MockKAnnotations.init(this) + every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState + + every { mockServiceConnectionManager.connectionState } returns serviceConnectionState + every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy + + tunnelStateNotificationUseCase = + TunnelStateNotificationUseCase(serviceConnectionManager = mockServiceConnectionManager) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `ensure notifications are empty by default`() = runTest { + // Arrange, Act, Assert + tunnelStateNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } } + } + + @Test + fun `ensure TunnelState with error will produce TunnelStateError notification`() = runTest { + tunnelStateNotificationUseCase.notifications().test { + // Arrange, Act + assertEquals(emptyList(), awaitItem()) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + val errorState: ErrorState = mockk() + eventNotifierTunnelUiState.notify(TunnelState.Error(errorState)) + + // Assert + assertEquals(listOf(InAppNotification.TunnelStateError(errorState)), awaitItem()) + } + } + + @Test + fun `ensure disconnecting TunnelState with blocking will produce TunnelStateBlocked notification`() = + runTest { + tunnelStateNotificationUseCase.notifications().test { + // Arrange, Act + assertEquals(emptyList(), awaitItem()) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + eventNotifierTunnelUiState.notify( + TunnelState.Disconnecting(ActionAfterDisconnect.Block) + ) + + // Assert + assertEquals(listOf(InAppNotification.TunnelStateBlocked), awaitItem()) + } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt new file mode 100644 index 0000000000..5aba70c938 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt @@ -0,0 +1,114 @@ +package net.mullvad.mullvadvpn.usecase + +import app.cash.turbine.test +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.repository.InAppNotification +import net.mullvad.mullvadvpn.ui.VersionInfo +import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState +import net.mullvad.mullvadvpn.util.appVersionCallbackFlow +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class VersionNotificationUseCaseTest { + @get:Rule val testCoroutineRule = TestCoroutineRule() + + private val mockServiceConnectionManager: ServiceConnectionManager = mockk() + private lateinit var mockAppVersionInfoCache: AppVersionInfoCache + private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() + + private val serviceConnectionState = + MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected) + private val versionInfo = + MutableStateFlow( + VersionInfo( + currentVersion = null, + upgradeVersion = null, + isOutdated = false, + isSupported = true + ) + ) + private lateinit var versionNotificationUseCase: VersionNotificationUseCase + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkStatic(CACHE_EXTENSION_CLASS) + mockAppVersionInfoCache = + mockk<AppVersionInfoCache>().apply { + every { appVersionCallbackFlow() } returns versionInfo + } + + every { mockServiceConnectionManager.connectionState } returns serviceConnectionState + every { mockServiceConnectionContainer.appVersionInfoCache } returns mockAppVersionInfoCache + every { mockAppVersionInfoCache.onUpdate = any() } answers {} + + versionNotificationUseCase = + VersionNotificationUseCase( + serviceConnectionManager = mockServiceConnectionManager, + isVersionInfoNotificationEnabled = true + ) + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `ensure notifications are empty by default`() = runTest { + // Arrange, Act, Assert + versionNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } } + } + + @Test + fun `ensure UpdateAvailable notification is created`() = runTest { + versionNotificationUseCase.notifications().test { + // Arrange, Act + val upgradeVersionInfo = + VersionInfo("1.0", "1.1", isOutdated = true, isSupported = true) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + awaitItem() + versionInfo.value = upgradeVersionInfo + + // Assert + assertEquals(awaitItem(), listOf(InAppNotification.UpdateAvailable(upgradeVersionInfo))) + } + } + + @Test + fun `ensure UnsupportedVersion notification is created`() = runTest { + versionNotificationUseCase.notifications().test { + // Arrange, Act + val upgradeVersionInfo = VersionInfo("1.0", "", isOutdated = false, isSupported = false) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + awaitItem() + versionInfo.value = upgradeVersionInfo + + // Assert + assertEquals( + awaitItem(), + listOf(InAppNotification.UnsupportedVersion(upgradeVersionInfo)) + ) + } + } + + companion object { + private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt" + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt index bddaee353e..5839e575c1 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt @@ -11,12 +11,12 @@ import io.mockk.unmockkAll import io.mockk.verify import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertTrue import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest -import net.mullvad.mullvadvpn.compose.state.ConnectNotificationState import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.model.AccountExpiry @@ -27,6 +27,8 @@ import net.mullvad.mullvadvpn.relaylist.RelayCountry import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.repository.InAppNotification +import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache @@ -42,8 +44,6 @@ import net.mullvad.mullvadvpn.util.appVersionCallbackFlow import net.mullvad.talpid.tunnel.ErrorState import net.mullvad.talpid.tunnel.ErrorStateCause import net.mullvad.talpid.util.EventNotifier -import org.joda.time.DateTime -import org.joda.time.ReadableInstant import org.junit.After import org.junit.Before import org.junit.Rule @@ -68,6 +68,7 @@ class ConnectViewModelTest { ) private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing) private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial) + private val notifications = MutableStateFlow<List<InAppNotification>>(emptyList()) // Service connections private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() @@ -83,6 +84,9 @@ class ConnectViewModelTest { // Device Repository private val mockDeviceRepository: DeviceRepository = mockk() + // In App Notifications + private val mockInAppNotificationController: InAppNotificationController = mockk() + // Captures private val locationSlot = slot<((GeoIpLocation?) -> Unit)>() private val relaySlot = slot<(List<RelayCountry>, RelayItem?) -> Unit>() @@ -111,6 +115,8 @@ class ConnectViewModelTest { every { mockDeviceRepository.deviceState } returns deviceState + every { mockInAppNotificationController.notifications } returns notifications + every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState every { mockConnectionProxy.onStateChange } returns eventNotifierTunnelRealState @@ -126,7 +132,8 @@ class ConnectViewModelTest { serviceConnectionManager = mockServiceConnectionManager, accountRepository = mockAccountRepository, deviceRepository = mockDeviceRepository, - isVersionInfoNotificationEnabled = true + inAppNotificationController = mockInAppNotificationController, + newDeviceNotificationUseCase = mockk() ) } @@ -144,8 +151,6 @@ class ConnectViewModelTest { @Test fun testTunnelInfoExpandedUpdate() = runTest(testCoroutineRule.testDispatcher) { - val expectedResult = true - viewModel.uiState.test { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = @@ -154,7 +159,7 @@ class ConnectViewModelTest { relaySlot.captured.invoke(mockk(), mockk()) viewModel.toggleTunnelInfoExpansion() val result = awaitItem() - assertEquals(expectedResult, result.isTunnelInfoExpanded) + assertTrue(result.isTunnelInfoExpanded) } } @@ -288,34 +293,14 @@ class ConnectViewModelTest { } @Test - fun testBlockingNotificationState() = - runTest(testCoroutineRule.testDispatcher) { - // Arrange - val expectedConnectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationBlocked - val tunnelUiState = TunnelState.Connecting(null, null) - - // Act, Assert - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) - relaySlot.captured.invoke(mockk(), mockk()) - eventNotifierTunnelUiState.notify(tunnelUiState) - val result = awaitItem() - assertEquals(expectedConnectNotificationState, result.connectNotificationState) - } - } - - @Test fun testErrorNotificationState() = runTest(testCoroutineRule.testDispatcher) { // Arrange val mockErrorState: ErrorState = mockk() val expectedConnectNotificationState = - ConnectNotificationState.ShowTunnelStateNotificationError(mockErrorState) + InAppNotification.TunnelStateError(mockErrorState) val tunnelUiState = TunnelState.Error(mockErrorState) + notifications.value = listOf(expectedConnectNotificationState) // Act, Assert viewModel.uiState.test { @@ -326,53 +311,7 @@ class ConnectViewModelTest { relaySlot.captured.invoke(mockk(), mockk()) eventNotifierTunnelUiState.notify(tunnelUiState) val result = awaitItem() - assertEquals(expectedConnectNotificationState, result.connectNotificationState) - } - } - - @Test - fun testVersionInfoNotificationState() = - runTest(testCoroutineRule.testDispatcher) { - // Arrange - val mockVersionInfo: VersionInfo = mockk() - val expectedConnectNotificationState = - ConnectNotificationState.ShowVersionInfoNotification(mockVersionInfo) - every { mockVersionInfo.isOutdated } returns true - - // Act, Assert - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) - relaySlot.captured.invoke(mockk(), mockk()) - versionInfo.value = mockVersionInfo - val result = awaitItem() - assertEquals(expectedConnectNotificationState, result.connectNotificationState) - } - } - - @Test - fun testAccountExpiryNotificationState() = - runTest(testCoroutineRule.testDispatcher) { - // Arrange - val mockDateTime: DateTime = mockk() - val expectedConnectNotificationState = - ConnectNotificationState.ShowAccountExpiryNotification(mockDateTime) - every { mockDateTime.isBefore(any<ReadableInstant>()) } returns true - every { mockDateTime.toInstant().millis } returns 0 - - // Act, Assert - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) - relaySlot.captured.invoke(mockk(), mockk()) - accountExpiryState.value = AccountExpiry.Available(mockDateTime) - - val result = awaitItem() - assertEquals(expectedConnectNotificationState, result.connectNotificationState) + assertEquals(expectedConnectNotificationState, result.inAppNotification) } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt index 744989a922..2ada5bf767 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt @@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.model.DeviceListEvent import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -34,6 +35,7 @@ class LoginViewModelTest { @MockK private lateinit var mockedAccountRepository: AccountRepository @MockK private lateinit var mockedDeviceRepository: DeviceRepository + @MockK private lateinit var mockedNewDeviceNotificationUseCase: NewDeviceNotificationUseCase private lateinit var loginViewModel: LoginViewModel private val accountHistoryTestEvents = MutableStateFlow<AccountHistory>(AccountHistory.Missing) @@ -44,11 +46,13 @@ class LoginViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) every { mockedAccountRepository.accountHistory } returns accountHistoryTestEvents + every { mockedNewDeviceNotificationUseCase.newDeviceCreated() } returns Unit loginViewModel = LoginViewModel( mockedAccountRepository, mockedDeviceRepository, + mockedNewDeviceNotificationUseCase, UnconfinedTestDispatcher() ) } |
