summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson90@gmail.com>2023-12-14 16:23:09 +0100
committerAlbin <albin@mullvad.net>2023-12-14 16:54:21 +0100
commitf33b1f76eac937b579ef589cc047da8f3421f630 (patch)
tree89e54908a6c5f7162813213bec00bfa425e521a0 /android
parent0d4451264d129bc6bcc8ae30bf12dc807f8ab3bc (diff)
downloadmullvadvpn-f33b1f76eac937b579ef589cc047da8f3421f630.tar.xz
mullvadvpn-f33b1f76eac937b579ef589cc047da8f3421f630.zip
Add OutOfTimeUseCase
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt77
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt128
2 files changed, 205 insertions, 0 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt
new file mode 100644
index 0000000000..ba7ce83172
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt
@@ -0,0 +1,77 @@
+package net.mullvad.mullvadvpn.usecase
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import net.mullvad.mullvadvpn.lib.ipc.Event
+import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
+import net.mullvad.mullvadvpn.lib.ipc.events
+import net.mullvad.mullvadvpn.model.AccountExpiry
+import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.repository.AccountRepository
+import net.mullvad.talpid.tunnel.ErrorStateCause
+import org.joda.time.DateTime
+
+const val accountRefreshIntervalMillis = 60L * 1000L // 1 minute
+const val bufferTimeMillis = 60L * 1000L // 1 minute
+
+class OutOfTimeUseCase(
+ private val repository: AccountRepository,
+ private val messageHandler: MessageHandler
+) {
+
+ fun isOutOfTime(): Flow<Boolean?> =
+ combine(pastAccountExpiry(), isTunnelBlockedBecauseOutOfTime()) {
+ accountExpiryHasPast,
+ tunnelOutOfTime ->
+ reduce(accountExpiryHasPast, tunnelOutOfTime)
+ }
+ .distinctUntilChanged()
+
+ private fun reduce(vararg outOfTimeProperty: Boolean?): Boolean? =
+ when {
+ // If any advertises as out of time
+ outOfTimeProperty.any { it == true } -> true
+ // If all advertise as not out of time
+ outOfTimeProperty.all { it == false } -> false
+ // If some are unknown
+ else -> null
+ }
+
+ private fun isTunnelBlockedBecauseOutOfTime() =
+ messageHandler
+ .events<Event.TunnelStateChange>()
+ .map { it.tunnelState.isTunnelErrorStateDueToExpiredAccount() }
+ .onStart { emit(false) }
+
+ private fun TunnelState.isTunnelErrorStateDueToExpiredAccount(): Boolean {
+ return ((this as? TunnelState.Error)?.errorState?.cause as? ErrorStateCause.AuthFailed)
+ ?.isCausedByExpiredAccount()
+ ?: false
+ }
+
+ private fun pastAccountExpiry(): Flow<Boolean?> =
+ combine(
+ repository.accountExpiryState.map {
+ if (it is AccountExpiry.Available) {
+ it.date()
+ } else {
+ null
+ }
+ },
+ timeFlow()
+ ) { expiryDate, time ->
+ expiryDate?.isBefore(time.plus(bufferTimeMillis))
+ }
+
+ private fun timeFlow() = flow {
+ while (true) {
+ emit(DateTime.now())
+ delay(accountRefreshIntervalMillis)
+ }
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt
new file mode 100644
index 0000000000..74683813ae
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt
@@ -0,0 +1,128 @@
+package net.mullvad.mullvadvpn.usecase
+
+import app.cash.turbine.test
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.test.assertEquals
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.ipc.Event
+import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
+import net.mullvad.mullvadvpn.lib.ipc.events
+import net.mullvad.mullvadvpn.model.AccountExpiry
+import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.repository.AccountRepository
+import net.mullvad.talpid.tunnel.ErrorState
+import net.mullvad.talpid.tunnel.ErrorStateCause
+import org.joda.time.DateTime
+import org.junit.Before
+import org.junit.Test
+
+class OutOfTimeUseCaseTest {
+ private val mockAccountRepository: AccountRepository = mockk()
+ private val mockMessageHandler: MessageHandler = mockk()
+
+ private val events = MutableSharedFlow<Event.TunnelStateChange>()
+ private val expiry = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
+
+ lateinit var outOfTimeUseCase: OutOfTimeUseCase
+
+ @Before
+ fun setup() {
+ every { mockAccountRepository.accountExpiryState } returns expiry
+ every { mockMessageHandler.events<Event.TunnelStateChange>() } returns events
+ outOfTimeUseCase = OutOfTimeUseCase(mockAccountRepository, mockMessageHandler)
+ }
+
+ @Test
+ fun `No events should result in no expiry`() = runTest {
+ // Arrange
+ // Act, Assert
+ outOfTimeUseCase.isOutOfTime().test { assertEquals(null, awaitItem()) }
+ }
+
+ @Test
+ fun `Tunnel is blocking because out of time should emit true`() = runTest {
+ // Arrange
+ // Act, Assert
+ val errorStateCause = ErrorStateCause.AuthFailed("[EXPIRED_ACCOUNT]")
+ val tunnelStateError = TunnelState.Error(ErrorState(errorStateCause, true))
+ val errorChange = Event.TunnelStateChange(tunnelStateError)
+
+ outOfTimeUseCase.isOutOfTime().test {
+ assertEquals(null, awaitItem())
+ events.emit(errorChange)
+ assertEquals(true, awaitItem())
+ }
+ }
+
+ @Test
+ fun `Tunnel is connected should emit false`() = runTest {
+ // Arrange
+ val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1))
+ val tunnelStateChanges =
+ listOf(
+ TunnelState.Disconnected,
+ TunnelState.Connected(mockk(), null),
+ TunnelState.Connecting(null, null),
+ TunnelState.Disconnecting(mockk()),
+ TunnelState.Error(ErrorState(ErrorStateCause.StartTunnelError, false)),
+ )
+ .map(Event::TunnelStateChange)
+
+ // Act, Assert
+ outOfTimeUseCase.isOutOfTime().test {
+ assertEquals(null, awaitItem())
+ events.emit(tunnelStateChanges.first())
+ expiry.emit(expiredAccountExpiry)
+ assertEquals(false, awaitItem())
+
+ tunnelStateChanges.forEach { events.emit(it) }
+
+ // Should not emit again
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `Account expiry that has expired should emit true`() = runTest {
+ // Arrange
+ val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().minusDays(1))
+ // Act, Assert
+ outOfTimeUseCase.isOutOfTime().test {
+ assertEquals(null, awaitItem())
+ expiry.emit(expiredAccountExpiry)
+ assertEquals(true, awaitItem())
+ }
+ }
+
+ @Test
+ fun `Account expiry that has not expired should emit false`() = runTest {
+ // Arrange
+ val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1))
+
+ // Act, Assert
+ outOfTimeUseCase.isOutOfTime().test {
+ assertEquals(null, awaitItem())
+ expiry.emit(expiredAccountExpiry)
+ assertEquals(false, awaitItem())
+ }
+ }
+
+ @Test
+ fun `Account that expires without new expiry event`() = runTest {
+ // Arrange
+ val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusSeconds(62))
+
+ // Act, Assert
+ outOfTimeUseCase.isOutOfTime().test {
+ // Initial event
+ assertEquals(null, awaitItem())
+
+ expiry.emit(expiredAccountExpiry)
+ assertEquals(false, awaitItem())
+ assertEquals(true, awaitItem())
+ }
+ }
+}