diff options
| author | Albin <albin@mullvad.net> | 2024-08-23 10:27:49 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2024-08-23 10:27:49 +0200 |
| commit | 97ee66d64d9e5ef9265eae3fe74464d1fc807dd4 (patch) | |
| tree | 8506abc903055521c1110e0ec8cb428e1431c6ba /android | |
| parent | 67486d316b94262cb2e478765f4234b5d12afcba (diff) | |
| parent | 4c7a6fa7941519ea637345cf6521edd067a6aa3a (diff) | |
| download | mullvadvpn-97ee66d64d9e5ef9265eae3fe74464d1fc807dd4.tar.xz mullvadvpn-97ee66d64d9e5ef9265eae3fe74464d1fc807dd4.zip | |
Merge branch 'ensure-all-potential-grpc-errors-are-wrapped-droid-1170'
Diffstat (limited to 'android')
20 files changed, 235 insertions, 53 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt index 6969e56518..bd64cc3561 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt @@ -57,6 +57,7 @@ import net.mullvad.mullvadvpn.compose.transitions.SlideInFromBottomTransition import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect import net.mullvad.mullvadvpn.compose.util.SecureScreenWhileInView import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle +import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.model.AccountNumber import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus @@ -164,6 +165,7 @@ fun AccountScreen( val snackbarHostState = remember { SnackbarHostState() } val copyTextString = stringResource(id = R.string.copied_mullvad_account_number) + val errorString = stringResource(id = R.string.error_occurred) val copyToClipboard = createCopyToClipboardHandle(snackbarHostState = snackbarHostState) val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() LaunchedEffectCollect(uiSideEffect) { sideEffect -> @@ -173,6 +175,8 @@ fun AccountScreen( openAccountPage(sideEffect.token) is AccountViewModel.UiSideEffect.CopyAccountNumber -> launch { copyToClipboard(sideEffect.accountNumber, copyTextString) } + AccountViewModel.UiSideEffect.GenericError -> + snackbarHostState.showSnackbarImmediately(message = errorString) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index 8a31520aad..1758a31432 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -73,6 +75,7 @@ import net.mullvad.mullvadvpn.compose.textfield.mullvadWhiteTextFieldColors import net.mullvad.mullvadvpn.compose.transitions.LoginTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.compose.util.accountNumberVisualTransformation +import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.viewmodel.LoginUiSideEffect @@ -126,6 +129,8 @@ fun Login( } } + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } CollectSideEffectWithLifecycle(vm.uiSideEffect) { when (it) { LoginUiSideEffect.NavigateToWelcome -> @@ -147,21 +152,27 @@ fun Login( launchSingleTop = true popUpTo(NavGraphs.root) { inclusive = true } } + LoginUiSideEffect.GenericError -> + snackbarHostState.showSnackbarImmediately( + message = context.getString(R.string.error_occurred), + ) } } LoginScreen( - state, - vm::login, - vm::createAccount, - vm::clearAccountHistory, - vm::onAccountNumberChange, - dropUnlessResumed { navigator.navigate(SettingsDestination) } + state = state, + snackbarHostState = snackbarHostState, + onLoginClick = vm::login, + onCreateAccountClick = vm::createAccount, + onDeleteHistoryClick = vm::clearAccountHistory, + onAccountNumberChange = vm::onAccountNumberChange, + onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) } ) } @Composable private fun LoginScreen( state: LoginUiState, + snackbarHostState: SnackbarHostState = SnackbarHostState(), onLoginClick: (String) -> Unit = {}, onCreateAccountClick: () -> Unit = {}, onDeleteHistoryClick: () -> Unit = {}, @@ -169,6 +180,7 @@ private fun LoginScreen( onSettingsClick: () -> Unit = {}, ) { ScaffoldWithTopBar( + snackbarHostState = snackbarHostState, topBarColor = MaterialTheme.colorScheme.primary, iconTintColor = MaterialTheme.colorScheme.onPrimary, onSettingsClicked = onSettingsClick, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt index eea7a20e47..7b5b4bff55 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -11,11 +11,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -49,6 +52,7 @@ import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState import net.mullvad.mullvadvpn.compose.test.OUT_OF_TIME_SCREEN_TITLE_TEST_TAG import net.mullvad.mullvadvpn.compose.transitions.HomeTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle +import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.lib.model.ErrorStateCause import net.mullvad.mullvadvpn.lib.model.TunnelState @@ -135,6 +139,8 @@ fun OutOfTime( } } + val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() CollectSideEffectWithLifecycle(vm.uiSideEffect, Lifecycle.State.RESUMED) { uiSideEffect -> when (uiSideEffect) { @@ -145,11 +151,16 @@ fun OutOfTime( launchSingleTop = true popUpTo(NavGraphs.root) { inclusive = true } } + OutOfTimeViewModel.UiSideEffect.GenericError -> + snackbarHostState.showSnackbarImmediately( + message = context.getString(R.string.error_occurred) + ) } } OutOfTimeScreen( state = state, + snackbarHostState = snackbarHostState, onSitePaymentClick = vm::onSitePaymentClick, onRedeemVoucherClick = dropUnlessResumed { navigator.navigate(RedeemVoucherDestination) }, onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }, @@ -165,6 +176,7 @@ fun OutOfTime( @Composable fun OutOfTimeScreen( state: OutOfTimeUiState, + snackbarHostState: SnackbarHostState = SnackbarHostState(), onDisconnectClick: () -> Unit = {}, onSitePaymentClick: () -> Unit = {}, onRedeemVoucherClick: () -> Unit = {}, @@ -176,6 +188,7 @@ fun OutOfTimeScreen( val scrollState = rememberScrollState() ScaffoldWithTopBarAndDeviceName( + snackbarHostState = snackbarHostState, topBarColor = if (state.tunnelState.isSecured()) { MaterialTheme.colorScheme.tertiary diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt index bb376c09f9..e53ec044cc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -57,6 +58,7 @@ import net.mullvad.mullvadvpn.compose.state.WelcomeUiState import net.mullvad.mullvadvpn.compose.transitions.HomeTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle +import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces import net.mullvad.mullvadvpn.lib.model.AccountNumber import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct @@ -131,6 +133,8 @@ fun Welcome( } } + val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() CollectSideEffectWithLifecycle(sideEffect = vm.uiSideEffect, Lifecycle.State.RESUMED) { uiSideEffect -> @@ -141,11 +145,16 @@ fun Welcome( launchSingleTop = true popUpTo(NavGraphs.root) { inclusive = true } } + WelcomeViewModel.UiSideEffect.GenericError -> + snackbarHostState.showSnackbarImmediately( + message = context.getString(R.string.error_occurred) + ) } } WelcomeScreen( state = state, + snackbarHostState = snackbarHostState, onSitePaymentClick = dropUnlessResumed { vm.onSitePaymentClick() }, onRedeemVoucherClick = dropUnlessResumed { navigator.navigate(RedeemVoucherDestination) }, onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }, @@ -163,6 +172,7 @@ fun Welcome( @Composable fun WelcomeScreen( state: WelcomeUiState, + snackbarHostState: SnackbarHostState = SnackbarHostState(), onSitePaymentClick: () -> Unit, onRedeemVoucherClick: () -> Unit, onSettingsClick: () -> Unit, @@ -173,7 +183,6 @@ fun WelcomeScreen( navigateToVerificationPendingDialog: () -> Unit ) { val scrollState = rememberScrollState() - val snackbarHostState = remember { SnackbarHostState() } ScaffoldWithTopBar( topBarColor = MaterialTheme.colorScheme.primary, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt index b830d00c60..f8f66563e8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt @@ -75,8 +75,12 @@ class AccountViewModel( fun onLogoutClick() { viewModelScope.launch { - accountRepository.logout() - _uiSideEffect.send(UiSideEffect.NavigateToLogin) + accountRepository + .logout() + .fold( + { _uiSideEffect.send(UiSideEffect.GenericError) }, + { _uiSideEffect.send(UiSideEffect.NavigateToLogin) } + ) } } @@ -127,6 +131,8 @@ class AccountViewModel( UiSideEffect() data class CopyAccountNumber(val accountNumber: String) : UiSideEffect() + + data object GenericError : UiSideEffect() } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt index d1f4b3713b..325ee42b43 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt @@ -123,11 +123,19 @@ class ConnectViewModel( } fun onDisconnectClick() { - viewModelScope.launch { connectionProxy.disconnect() } + viewModelScope.launch { + connectionProxy.disconnect().onLeft { + _uiSideEffect.send(UiSideEffect.ConnectError.Generic) + } + } } fun onReconnectClick() { - viewModelScope.launch { connectionProxy.reconnect() } + viewModelScope.launch { + connectionProxy.reconnect().onLeft { + _uiSideEffect.send(UiSideEffect.ConnectError.Generic) + } + } } fun onConnectClick() { @@ -156,7 +164,11 @@ class ConnectViewModel( } fun onCancelClick() { - viewModelScope.launch { connectionProxy.disconnect() } + viewModelScope.launch { + connectionProxy.disconnect().onLeft { + _uiSideEffect.send(UiSideEffect.ConnectError.Generic) + } + } } fun onManageAccountClick() { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt index 30bea42cfe..3c02c92917 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt @@ -42,6 +42,8 @@ sealed interface LoginUiSideEffect { data object NavigateToOutOfTime : LoginUiSideEffect data class TooManyDevices(val accountNumber: AccountNumber) : LoginUiSideEffect + + data object GenericError : LoginUiSideEffect } class LoginViewModel( @@ -78,9 +80,15 @@ class LoginViewModel( fun clearAccountHistory() = viewModelScope.launch { - accountRepository.clearAccountHistory() - _mutableAccountHistory.update { null } - _mutableAccountHistory.update { accountRepository.fetchAccountHistory() } + accountRepository + .clearAccountHistory() + .fold( + { _uiSideEffect.send(LoginUiSideEffect.GenericError) }, + { + _mutableAccountHistory.update { null } + _mutableAccountHistory.update { accountRepository.fetchAccountHistory() } + } + ) } fun createAccount() { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt index b2738a56ae..dfa4e6e3cc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt @@ -22,6 +22,7 @@ import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase import net.mullvad.mullvadvpn.usecase.PaymentUseCase import net.mullvad.mullvadvpn.util.isSuccess import net.mullvad.mullvadvpn.util.toPaymentState +import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel.UiSideEffect class OutOfTimeViewModel( private val accountRepository: AccountRepository, @@ -71,7 +72,9 @@ class OutOfTimeViewModel( } fun onDisconnectClick() { - viewModelScope.launch { connectionProxy.disconnect() } + viewModelScope.launch { + connectionProxy.disconnect().onLeft { _uiSideEffect.send(UiSideEffect.GenericError) } + } } private fun verifyPurchases() { @@ -118,5 +121,7 @@ class OutOfTimeViewModel( data class OpenAccountView(val token: WebsiteAuthToken?) : UiSideEffect data object OpenConnectScreen : UiSideEffect + + data object GenericError : UiSideEffect } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt index 525c6ca54e..e532045ae2 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt @@ -78,7 +78,9 @@ class WelcomeViewModel( } fun onDisconnectClick() { - viewModelScope.launch { connectionProxy.disconnect() } + viewModelScope.launch { + connectionProxy.disconnect().onLeft { _uiSideEffect.send(UiSideEffect.GenericError) } + } } private fun verifyPurchases() { @@ -118,6 +120,8 @@ class WelcomeViewModel( data class OpenAccountView(val token: WebsiteAuthToken?) : UiSideEffect data object OpenConnectScreen : UiSideEffect + + data object GenericError : UiSideEffect } companion object { diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt index 706d8031e7..d239e01331 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel import android.app.Activity import app.cash.turbine.test +import arrow.core.right import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -93,6 +94,9 @@ class AccountViewModelTest { @Test fun `onLogoutClick should invoke logout on AccountRepository`() { + // Arrange + coEvery { mockAccountRepository.logout() } returns Unit.right() + // Act viewModel.onLogoutClick() 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 e6803d4e08..50a16d1432 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 @@ -234,7 +234,7 @@ class ConnectViewModelTest { @Test fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true + coEvery { mockConnectionProxy.disconnect() } returns true.right() // Act viewModel.onDisconnectClick() @@ -246,7 +246,7 @@ class ConnectViewModelTest { @Test fun `onReconnectClick should invoke reconnect on ConnectionProxy`() = runTest { // Arrange - coEvery { mockConnectionProxy.reconnect() } returns true + coEvery { mockConnectionProxy.reconnect() } returns true.right() // Act viewModel.onReconnectClick() @@ -270,7 +270,7 @@ class ConnectViewModelTest { @Test fun `onCancelClick should invoke disconnect on ConnectionProxy`() = runTest { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true + coEvery { mockConnectionProxy.disconnect() } returns true.right() // Act viewModel.onCancelClick() diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt index b63f59b302..51ea31540c 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt @@ -1,14 +1,13 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test +import arrow.core.right import io.mockk.MockKAnnotations -import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just import io.mockk.mockk import io.mockk.unmockkAll import kotlinx.coroutines.flow.MutableSharedFlow @@ -70,8 +69,8 @@ class DeviceRevokedViewModelTest { @Test fun `onGoToLoginClicked should invoke logout on AccountRepository`() { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true - coEvery { mockedAccountRepository.logout() } just Runs + coEvery { mockConnectionProxy.disconnect() } returns true.right() + coEvery { mockedAccountRepository.logout() } returns Unit.right() // Act viewModel.onGoToLoginClicked() @@ -83,8 +82,8 @@ class DeviceRevokedViewModelTest { @Test fun `onGoToLoginClicked should invoke disconnect before logout when connected`() { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true - coEvery { mockedAccountRepository.logout() } just Runs + coEvery { mockConnectionProxy.disconnect() } returns true.right() + coEvery { mockedAccountRepository.logout() } returns Unit.right() // Act viewModel.onGoToLoginClicked() 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 33eb3bdc3c..a9905e9506 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 @@ -208,6 +208,9 @@ class LoginViewModelTest { @Test fun `clearAccountHistory should invoke clearAccountHistory on AccountRepository`() = runTest { + // Arrange + coEvery { mockedAccountRepository.clearAccountHistory() } returns Unit.right() + // Act, Assert loginViewModel.clearAccountHistory() coVerify { mockedAccountRepository.clearAccountHistory() } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt index bd26effe82..886cb58fda 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.viewModelScope import app.cash.turbine.test +import arrow.core.right import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -146,7 +147,7 @@ class OutOfTimeViewModelTest { @Test fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true + coEvery { mockConnectionProxy.disconnect() } returns true.right() // Act viewModel.onDisconnectClick() diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt index 1c7a7d0e3b..9ef1455bb7 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.viewModelScope import app.cash.turbine.test +import arrow.core.right import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -217,7 +218,7 @@ class WelcomeViewModelTest { @Test fun `when on disconnect click is called should call connection proxy disconnect`() = runTest { // Arrange - coEvery { mockConnectionProxy.disconnect() } returns true + coEvery { mockConnectionProxy.disconnect() } returns true.right() // Act viewModel.onDisconnectClick() diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt index 36460ae1fa..c10f3b58e6 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt @@ -53,6 +53,7 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodSetting import net.mullvad.mullvadvpn.lib.model.AppId import net.mullvad.mullvadvpn.lib.model.AppVersionInfo as ModelAppVersionInfo +import net.mullvad.mullvadvpn.lib.model.ClearAccountHistoryError import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError import net.mullvad.mullvadvpn.lib.model.ConnectError import net.mullvad.mullvadvpn.lib.model.Constraint @@ -77,6 +78,7 @@ import net.mullvad.mullvadvpn.lib.model.GetDeviceListError import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError import net.mullvad.mullvadvpn.lib.model.GetVersionInfoError import net.mullvad.mullvadvpn.lib.model.LoginAccountError +import net.mullvad.mullvadvpn.lib.model.LogoutAccountError import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings @@ -245,16 +247,19 @@ class ManagementService( suspend fun getDevice(): Either<GetDeviceStateError, ModelDeviceState> = Either.catch { grpc.getDevice(Empty.getDefaultInstance()) } .map { it.toDomain() } + .onLeft { Logger.e("Get device error") } .mapLeft { GetDeviceStateError.Unknown(it) } suspend fun updateDevice(): Either<DeviceUpdateError, Unit> = Either.catch { grpc.updateDevice(Empty.getDefaultInstance()) } .mapEmpty() + .onLeft { Logger.e("Update device error") } .mapLeft { DeviceUpdateError(it) } suspend fun getDeviceList(token: AccountNumber): Either<GetDeviceListError, List<Device>> = Either.catch { grpc.listDevices(StringValue.of(token.value)) } .map { it.devicesList.map(ManagementInterface.Device::toDomain) } + .onLeft { Logger.e("Get device list error") } .mapLeft { GetDeviceListError.Unknown(it) } suspend fun removeDevice( @@ -270,15 +275,23 @@ class ManagementService( ) } .mapEmpty() + .onLeft { Logger.e("Remove device error") } .mapLeft { DeleteDeviceError.Unknown(it) } suspend fun connect(): Either<ConnectError, Boolean> = Either.catch { grpc.connectTunnel(Empty.getDefaultInstance()).value } + .onLeft { Logger.e("Connect error") } .mapLeft(ConnectError::Unknown) - suspend fun disconnect(): Boolean = grpc.disconnectTunnel(Empty.getDefaultInstance()).value + suspend fun disconnect(): Either<ConnectError, Boolean> = + Either.catch { grpc.disconnectTunnel(Empty.getDefaultInstance()).value } + .onLeft { Logger.e("Disconnect error") } + .mapLeft(ConnectError::Unknown) - suspend fun reconnect(): Boolean = grpc.reconnectTunnel(Empty.getDefaultInstance()).value + suspend fun reconnect(): Either<ConnectError, Boolean> = + Either.catch { grpc.reconnectTunnel(Empty.getDefaultInstance()).value } + .onLeft { Logger.e("Reconnect error") } + .mapLeft(ConnectError::Unknown) private suspend fun getTunnelState(): ModelTunnelState = grpc.getTunnelState(Empty.getDefaultInstance()).toDomain() @@ -296,11 +309,17 @@ class ManagementService( // will get 404 until the api have been published, thus we need to ignore error downstream. private suspend fun getVersionInfo(): Either<GetVersionInfoError, ModelAppVersionInfo> = Either.catch { grpc.getVersionInfo(Empty.getDefaultInstance()).toDomain() } + .onLeft { Logger.e("Get version info error") } .mapLeft { GetVersionInfoError.Unknown(it) } - suspend fun logoutAccount() { - grpc.logoutAccount(Empty.getDefaultInstance()) - } + private suspend fun getCurrentApiAccessMethod(): ApiAccessMethodSetting = + grpc.getCurrentApiAccessMethod(Empty.getDefaultInstance()).toDomain() + + suspend fun logoutAccount(): Either<LogoutAccountError, Unit> = + Either.catch { grpc.logoutAccount(Empty.getDefaultInstance()) } + .onLeft { Logger.e("Logout account error") } + .mapLeft(LogoutAccountError::Unknown) + .mapEmpty() suspend fun loginAccount(accountNumber: AccountNumber): Either<LoginAccountError, Unit> = Either.catch { grpc.loginAccount(StringValue.of(accountNumber.value)) } @@ -310,14 +329,19 @@ class ManagementService( Status.Code.RESOURCE_EXHAUSTED -> LoginAccountError.MaxDevicesReached(accountNumber) Status.Code.UNAVAILABLE -> LoginAccountError.RpcError - else -> LoginAccountError.Unknown(it) + else -> { + Logger.e("Unknown login account error") + LoginAccountError.Unknown(it) + } } } .mapEmpty() - suspend fun clearAccountHistory() { - grpc.clearAccountHistory(Empty.getDefaultInstance()) - } + suspend fun clearAccountHistory(): Either<ClearAccountHistoryError, Unit> = + Either.catch { grpc.clearAccountHistory(Empty.getDefaultInstance()) } + .onLeft { Logger.e("Clear account history error") } + .mapLeft(ClearAccountHistoryError::Unknown) + .mapEmpty() suspend fun getAccountHistory(): Either<GetAccountHistoryError, AccountNumber?> = Either.catch { @@ -328,6 +352,7 @@ class ManagementService( null } } + .onLeft { Logger.e("Get account history error") } .mapLeft(GetAccountHistoryError::Unknown) private suspend fun getInitialServiceState() { @@ -347,6 +372,7 @@ class ManagementService( accountNumber: AccountNumber ): Either<GetAccountDataError, AccountData> = Either.catch { grpc.getAccountData(StringValue.of(accountNumber.value)).toDomain() } + .onLeft { Logger.e("Get account data error") } .mapLeft(GetAccountDataError::Unknown) suspend fun createAccount(): Either<CreateAccountError, AccountNumber> = @@ -354,10 +380,12 @@ class ManagementService( val accountNumberStringValue = grpc.createNewAccount(Empty.getDefaultInstance()) AccountNumber(accountNumberStringValue.value) } + .onLeft { Logger.e("Create account error") } .mapLeft(CreateAccountError::Unknown) suspend fun setDnsOptions(dnsOptions: ModelDnsOptions): Either<SetDnsOptionsError, Unit> = Either.catch { grpc.setDnsOptions(dnsOptions.fromDomain()) } + .onLeft { Logger.e("Set dns options error") } .mapLeft(SetDnsOptionsError::Unknown) .mapEmpty() @@ -367,6 +395,7 @@ class ManagementService( val updated = DnsOptions.state.set(currentDnsOptions, dnsState) grpc.setDnsOptions(updated.fromDomain()) } + .onLeft { Logger.e("Set dns state error") } .mapLeft(SetDnsOptionsError::Unknown) .mapEmpty() @@ -380,6 +409,7 @@ class ManagementService( grpc.setDnsOptions(updatedDnsOptions.fromDomain()) } + .onLeft { Logger.e("Set custom dns error") } .mapLeft(SetDnsOptionsError::Unknown) .mapEmpty() @@ -391,6 +421,7 @@ class ManagementService( grpc.setDnsOptions(updatedDnsOptions.fromDomain()) updatedDnsOptions.customOptions.addresses.lastIndex } + .onLeft { Logger.e("Add custom dns error") } .mapLeft(SetDnsOptionsError::Unknown) suspend fun deleteCustomDns(index: Int): Either<SetDnsOptionsError, Unit> = @@ -404,16 +435,19 @@ class ManagementService( } grpc.setDnsOptions(updatedDnsOptions.fromDomain()) } + .onLeft { Logger.e("Delete custom dns error") } .mapLeft(SetDnsOptionsError::Unknown) .mapEmpty() suspend fun setWireguardMtu(value: Int): Either<SetWireguardMtuError, Unit> = Either.catch { grpc.setWireguardMtu(UInt32Value.of(value)) } + .onLeft { Logger.e("Set wireguard mtu error") } .mapLeft(SetWireguardMtuError::Unknown) .mapEmpty() suspend fun resetWireguardMtu(): Either<SetWireguardMtuError, Unit> = Either.catch { grpc.setWireguardMtu(UInt32Value.newBuilder().clearValue().build()) } + .onLeft { Logger.e("Reset wireguard mtu error") } .mapLeft(SetWireguardMtuError::Unknown) .mapEmpty() @@ -421,14 +455,10 @@ class ManagementService( value: ModelQuantumResistantState ): Either<SetWireguardQuantumResistantError, Unit> = Either.catch { grpc.setQuantumResistantTunnel(value.toDomain()) } + .onLeft { Logger.e("Set wireguard quantum resistant error") } .mapLeft(SetWireguardQuantumResistantError::Unknown) .mapEmpty() - // Todo needs to be more advanced - suspend fun setRelaySettings(value: RelaySettings) { - grpc.setRelaySettings(value.fromDomain()) - } - suspend fun setObfuscation( value: SelectedObfuscation ): Either<SetObfuscationOptionsError, Unit> = @@ -441,6 +471,7 @@ class ManagementService( } grpc.setObfuscationSettings(updatedObfuscationSettings.fromDomain()) } + .onLeft { Logger.e("Set obfuscation error") } .mapLeft(SetObfuscationOptionsError::Unknown) .mapEmpty() @@ -454,16 +485,19 @@ class ManagementService( } grpc.setObfuscationSettings(updatedSettings.fromDomain()) } + .onLeft { Logger.e("Set obfuscation port error") } .mapLeft(SetObfuscationOptionsError::Unknown) .mapEmpty() suspend fun setAutoConnect(isEnabled: Boolean): Either<SetAutoConnectError, Unit> = Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) } + .onLeft { Logger.e("Set auto connect error") } .mapLeft(SetAutoConnectError::Unknown) .mapEmpty() suspend fun setAllowLan(allow: Boolean): Either<SetAllowLanError, Unit> = Either.catch { grpc.setAllowLan(BoolValue.of(allow)) } + .onLeft { Logger.e("Set allow lan error") } .mapLeft(SetAllowLanError::Unknown) .mapEmpty() @@ -477,6 +511,7 @@ class ManagementService( ) grpc.setRelaySettings(updatedRelaySettings.fromDomain()) } + .onLeft { Logger.e("Set relay location error") } .mapLeft(SetRelayLocationError::Unknown) .mapEmpty() @@ -488,7 +523,10 @@ class ManagementService( .mapLeftStatus { when (it.status.code) { Status.Code.ALREADY_EXISTS -> CustomListAlreadyExists - else -> UnknownCustomListError(it) + else -> { + Logger.e("Unknown create custom list error") + UnknownCustomListError(it) + } } } @@ -497,18 +535,23 @@ class ManagementService( .mapLeftStatus { when (it.status.code) { Status.Code.ALREADY_EXISTS -> NameAlreadyExists(customList.name) - else -> UnknownCustomListError(it) + else -> { + Logger.e("Unknown update custom list error") + UnknownCustomListError(it) + } } } .mapEmpty() suspend fun deleteCustomList(id: CustomListId): Either<DeleteCustomListError, Unit> = Either.catch { grpc.deleteCustomList(StringValue.of(id.value)) } + .onLeft { Logger.e("Delete custom list error") } .mapLeft(::UnknownCustomListError) .mapEmpty() suspend fun clearAllRelayOverrides(): Either<ClearAllOverridesError, Unit> = Either.catch { grpc.clearAllRelayOverrides(Empty.getDefaultInstance()) } + .onLeft { Logger.e("Clear all relay overrides error") } .mapLeft(ClearAllOverridesError::Unknown) .mapEmpty() @@ -518,7 +561,10 @@ class ManagementService( when (it.status.code) { // Currently we only get invalid argument errors from daemon via gRPC Status.Code.INVALID_ARGUMENT -> SettingsPatchError.ParsePatch - else -> SettingsPatchError.ApplyPatch + else -> { + Logger.e("Unknown apply settings patch error") + SettingsPatchError.ApplyPatch + } } } .mapEmpty() @@ -532,6 +578,7 @@ class ManagementService( RelaySettings.relayConstraints.wireguardConstraints.set(relaySettings, value) grpc.setRelaySettings(updated.fromDomain()) } + .onLeft { Logger.e("Set wireguard constraints error") } .mapLeft(SetWireguardConstraintsError::Unknown) .mapEmpty() @@ -550,6 +597,7 @@ class ManagementService( } grpc.setRelaySettings(updated.fromDomain()) } + .onLeft { Logger.e("Set ownership and providers error") } .mapLeft(SetWireguardConstraintsError::Unknown) .mapEmpty() @@ -561,6 +609,7 @@ class ManagementService( val updated = RelaySettings.relayConstraints.ownership.set(relaySettings, ownership) grpc.setRelaySettings(updated.fromDomain()) } + .onLeft { Logger.e("Set ownership error") } .mapLeft(SetWireguardConstraintsError::Unknown) .mapEmpty() @@ -573,6 +622,7 @@ class ManagementService( RelaySettings.relayConstraints.providers.set(relaySettings, providersConstraint) grpc.setRelaySettings(updated.fromDomain()) } + .onLeft { Logger.e("Set providers error") } .mapLeft(SetWireguardConstraintsError::Unknown) .mapEmpty() @@ -587,26 +637,33 @@ class ManagementService( Status.Code.ALREADY_EXISTS, Status.Code.RESOURCE_EXHAUSTED -> RedeemVoucherError.VoucherAlreadyUsed Status.Code.UNAVAILABLE -> RedeemVoucherError.RpcError - else -> RedeemVoucherError.Unknown(it) + else -> { + Logger.e("Unknown submit voucher error") + RedeemVoucherError.Unknown(it) + } } } suspend fun initializePlayPurchase(): Either<PlayPurchaseInitError, PlayPurchasePaymentToken> = Either.catch { grpc.initPlayPurchase(Empty.getDefaultInstance()).toDomain() } + .onLeft { Logger.e("Initialize play purchase error") } .mapLeft { PlayPurchaseInitError.OtherError } suspend fun verifyPlayPurchase(purchase: PlayPurchase): Either<PlayPurchaseVerifyError, Unit> = Either.catch { grpc.verifyPlayPurchase(purchase.fromDomain()) } + .onLeft { Logger.e("Verify play purchase error") } .mapLeft { PlayPurchaseVerifyError.OtherError } .mapEmpty() suspend fun addSplitTunnelingApp(app: AppId): Either<AddSplitTunnelingAppError, Unit> = Either.catch { grpc.addSplitTunnelApp(StringValue.of(app.value)) } + .onLeft { Logger.e("Add split tunneling app error") } .mapLeft(AddSplitTunnelingAppError::Unknown) .mapEmpty() suspend fun removeSplitTunnelingApp(app: AppId): Either<RemoveSplitTunnelingAppError, Unit> = Either.catch { grpc.removeSplitTunnelApp(StringValue.of(app.value)) } + .onLeft { Logger.e("Remove split tunneling app error") } .mapLeft(RemoveSplitTunnelingAppError::Unknown) .mapEmpty() @@ -614,17 +671,20 @@ class ManagementService( enabled: Boolean ): Either<RemoveSplitTunnelingAppError, Unit> = Either.catch { grpc.setSplitTunnelState(BoolValue.of(enabled)) } + .onLeft { Logger.e("Set split tunneling state error") } .mapLeft(RemoveSplitTunnelingAppError::Unknown) .mapEmpty() suspend fun getWebsiteAuthToken(): Either<Throwable, WebsiteAuthToken> = Either.catch { grpc.getWwwAuthToken(Empty.getDefaultInstance()) } + .onLeft { Logger.e("Get website auth token error") } .map { WebsiteAuthToken.fromString(it.value) } suspend fun addApiAccessMethod( newAccessMethodSetting: NewAccessMethodSetting ): Either<AddApiAccessMethodError, ApiAccessMethodId> = Either.catch { grpc.addApiAccessMethod(newAccessMethodSetting.fromDomain()) } + .onLeft { Logger.e("Add api access method error") } .mapLeft(AddApiAccessMethodError::Unknown) .map { ApiAccessMethodId.fromString(it.value) } @@ -632,6 +692,7 @@ class ManagementService( apiAccessMethodId: ApiAccessMethodId ): Either<RemoveApiAccessMethodError, Unit> = Either.catch { grpc.removeApiAccessMethod(apiAccessMethodId.fromDomain()) } + .onLeft { Logger.e("Remove api access method error") } .mapLeft(RemoveApiAccessMethodError::Unknown) .mapEmpty() @@ -639,6 +700,7 @@ class ManagementService( apiAccessMethodId: ApiAccessMethodId ): Either<SetApiAccessMethodError, Unit> = Either.catch { grpc.setApiAccessMethod(apiAccessMethodId.fromDomain()) } + .onLeft { Logger.e("Set api access method error") } .mapLeft(SetApiAccessMethodError::Unknown) .mapEmpty() @@ -646,16 +708,15 @@ class ManagementService( apiAccessMethodSetting: ApiAccessMethodSetting ): Either<UpdateApiAccessMethodError, Unit> = Either.catch { grpc.updateApiAccessMethod(apiAccessMethodSetting.fromDomain()) } + .onLeft { Logger.e("Update api access method error") } .mapLeft(::UnknownApiAccessMethodError) .mapEmpty() - private suspend fun getCurrentApiAccessMethod(): ApiAccessMethodSetting = - grpc.getCurrentApiAccessMethod(Empty.getDefaultInstance()).toDomain() - suspend fun testCustomApiAccessMethod( customProxy: ApiAccessMethod.CustomProxy ): Either<TestApiAccessMethodError, Unit> = Either.catch { grpc.testCustomApiAccessMethod(customProxy.fromDomain()) } + .onLeft { Logger.e("Test custom api access method error") } .mapLeftStatus { TestApiAccessMethodError.Grpc } .map { result -> either { ensure(result.value) { TestApiAccessMethodError.CouldNotAccess } } @@ -665,6 +726,7 @@ class ManagementService( apiAccessMethodId: ApiAccessMethodId ): Either<TestApiAccessMethodError, Unit> = Either.catch { grpc.testApiAccessMethodById(apiAccessMethodId.fromDomain()) } + .onLeft { Logger.e("Test api access method error") } .mapLeftStatus { TestApiAccessMethodError.Grpc } .map { result -> either { ensure(result.value) { TestApiAccessMethodError.CouldNotAccess } } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt new file mode 100644 index 0000000000..bd5cff4002 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface ClearAccountHistoryError { + data class Unknown(val t: Throwable) : ClearAccountHistoryError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt new file mode 100644 index 0000000000..da92b9216c --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface LogoutAccountError { + data class Unknown(val t: Throwable) : LogoutAccountError +} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt index a71dbe8efb..82f70e8140 100644 --- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt @@ -54,10 +54,8 @@ class AccountRepository( suspend fun login(accountNumber: AccountNumber): Either<LoginAccountError, Unit> = managementService.loginAccount(accountNumber) - suspend fun logout() { - managementService.logoutAccount() - _isNewAccount.update { false } - } + suspend fun logout() = + managementService.logoutAccount().onRight { _isNewAccount.update { false } } suspend fun fetchAccountHistory(): AccountNumber? = managementService.getAccountHistory().getOrNull() diff --git a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt new file mode 100644 index 0000000000..a649a3ac5d --- /dev/null +++ b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt @@ -0,0 +1,31 @@ +package net.mullvad.mullvadvpn.test.arch + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withPublicOrDefaultModifier +import com.lemonappdev.konsist.api.verify.assertTrue +import org.junit.jupiter.api.Test + +class ManagementServiceTest { + + @Test + fun `ensure all public functions are returning Either`() { + managementServiceClass() + .functions() + .withPublicOrDefaultModifier() + .filter { excludedFunctions().contains(it.name).not() } + .assertTrue { it.returnType?.name?.startsWith(EITHER_CLASS_NAME) == true } + } + + private fun managementServiceClass() = + Konsist.scopeFromProject().classes().first { it.name == MANAGEMENT_SERVICE_CLASS_NAME } + + private fun excludedFunctions() = setOf(START, STOP, ENTER_IDLE) + + companion object { + private const val MANAGEMENT_SERVICE_CLASS_NAME = "ManagementService" + private const val START = "start" + private const val STOP = "stop" + private const val ENTER_IDLE = "enterIdle" + private const val EITHER_CLASS_NAME = "Either" + } +} |
