diff options
| author | David Göransson <david.goransson90@gmail.com> | 2023-12-14 16:23:09 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2023-12-14 16:54:21 +0100 |
| commit | f33b1f76eac937b579ef589cc047da8f3421f630 (patch) | |
| tree | 89e54908a6c5f7162813213bec00bfa425e521a0 /android | |
| parent | 0d4451264d129bc6bcc8ae30bf12dc807f8ab3bc (diff) | |
| download | mullvadvpn-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.kt | 77 | ||||
| -rw-r--r-- | android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt | 128 |
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()) + } + } +} |
