diff options
38 files changed, 1035 insertions, 893 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt index d61fa31a8c..34a8a8908f 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt @@ -56,8 +56,7 @@ class RedeemVoucherDialogTest { val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true) setContentWithTheme { RedeemVoucherDialog( - uiState = - VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)), + uiState = VoucherDialogUiState(voucherState = VoucherDialogState.Success(0)), onVoucherInputChange = {}, onRedeem = {}, onDismiss = mockedClickHandler @@ -98,8 +97,7 @@ class RedeemVoucherDialogTest { // Arrange setContentWithTheme { RedeemVoucherDialog( - uiState = - VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Verifying), + uiState = VoucherDialogUiState(voucherState = VoucherDialogState.Verifying), onVoucherInputChange = {}, onRedeem = {}, onDismiss = {} @@ -116,8 +114,7 @@ class RedeemVoucherDialogTest { // Arrange setContentWithTheme { RedeemVoucherDialog( - uiState = - VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)), + uiState = VoucherDialogUiState(voucherState = VoucherDialogState.Success(0)), onVoucherInputChange = {}, onRedeem = {}, onDismiss = {} @@ -136,7 +133,7 @@ class RedeemVoucherDialogTest { RedeemVoucherDialog( uiState = VoucherDialogUiState( - voucherViewModelState = VoucherDialogState.Error(ERROR_MESSAGE) + voucherState = VoucherDialogState.Error(ERROR_MESSAGE) ), onVoucherInputChange = {}, onRedeem = {}, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt index 339de8f3a9..a9029a3758 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt @@ -119,14 +119,14 @@ fun RedeemVoucherDialog( ) { AlertDialog( title = { - if (uiState.voucherViewModelState !is VoucherDialogState.Success) + if (uiState.voucherState !is VoucherDialogState.Success) Text( text = stringResource(id = R.string.enter_voucher_code), ) }, confirmButton = { Column { - if (uiState.voucherViewModelState !is VoucherDialogState.Success) { + if (uiState.voucherState !is VoucherDialogState.Success) { VariantButton( text = stringResource(id = R.string.redeem), onClick = { onRedeem(uiState.voucherInput) }, @@ -138,13 +138,11 @@ fun RedeemVoucherDialog( text = stringResource( id = - if (uiState.voucherViewModelState is VoucherDialogState.Success) + if (uiState.voucherState is VoucherDialogState.Success) R.string.got_it else R.string.cancel ), - onClick = { - onDismiss(uiState.voucherViewModelState is VoucherDialogState.Success) - } + onClick = { onDismiss(uiState.voucherState is VoucherDialogState.Success) } ) } }, @@ -153,11 +151,9 @@ fun RedeemVoucherDialog( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - if (uiState.voucherViewModelState is VoucherDialogState.Success) { + if (uiState.voucherState is VoucherDialogState.Success) { val days: Int = - (uiState.voucherViewModelState.addedTime / - DateTimeConstants.SECONDS_PER_DAY) - .toInt() + (uiState.voucherState.addedTime / DateTimeConstants.SECONDS_PER_DAY).toInt() val message = stringResource( R.string.added_to_your_account, @@ -190,9 +186,7 @@ fun RedeemVoucherDialog( }, containerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.onBackground, - onDismissRequest = { - onDismiss(uiState.voucherViewModelState is VoucherDialogState.Success) - }, + onDismissRequest = { onDismiss(uiState.voucherState is VoucherDialogState.Success) }, properties = DialogProperties( securePolicy = @@ -257,7 +251,7 @@ private fun EnterVoucherBody( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.height(Dimens.listIconSize).fillMaxWidth() ) { - if (uiState.voucherViewModelState is VoucherDialogState.Verifying) { + if (uiState.voucherState is VoucherDialogState.Verifying) { MullvadCircularProgressIndicatorSmall() Text( text = stringResource(id = R.string.verifying_voucher), @@ -265,9 +259,9 @@ private fun EnterVoucherBody( color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.bodySmall ) - } else if (uiState.voucherViewModelState is VoucherDialogState.Error) { + } else if (uiState.voucherState is VoucherDialogState.Error) { Text( - text = uiState.voucherViewModelState.errorMessage, + text = uiState.voucherState.errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt index e719bda529..c143dda0e8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.compose.state data class VoucherDialogUiState( val voucherInput: String = "", - val voucherViewModelState: VoucherDialogState = VoucherDialogState.Default + val voucherState: VoucherDialogState = VoucherDialogState.Default ) { companion object { val INITIAL = VoucherDialogUiState() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt index 6a69a807f1..aa75c2403a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt @@ -10,7 +10,7 @@ fun PaymentAvailability.toPaymentState(): PaymentState = is PaymentAvailability.Error.Other -> PaymentState.Error.Generic is PaymentAvailability.ProductsAvailable -> PaymentState.PaymentAvailable(products) PaymentAvailability.ProductsUnavailable -> PaymentState.NoPayment - PaymentAvailability.NoProductsFounds -> PaymentState.NoProductsFounds + PaymentAvailability.NoProductsFound -> PaymentState.NoProductsFounds PaymentAvailability.Loading -> PaymentState.Loading // Unrecoverable error states PaymentAvailability.Error.DeveloperError, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt index b26429f18b..8022332650 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt @@ -49,7 +49,7 @@ class VoucherDialogViewModel( _shared .flatMapLatest { combine(vmState, voucherInput) { state, input -> - VoucherDialogUiState(voucherInput = input, voucherViewModelState = state) + VoucherDialogUiState(voucherInput = input, voucherState = state) } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), VoucherDialogUiState.INITIAL) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt index 1b37184915..62ceaa7ebe 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt @@ -23,7 +23,7 @@ class ApplicationsProviderTest { } @Test - fun test_get_apps() { + fun `fetch all apps should work`() { val launchWithInternetPackageName = "launch_with_internet_package_name" val launchWithoutInternetPackageName = "launch_without_internet_package_name" val nonLaunchWithInternetPackageName = "non_launch_with_internet_package_name" diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt index 6fe71ae443..1532328729 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt @@ -14,40 +14,40 @@ class RelayNameComparatorTest { } @Test - fun test_compare_respect_numbers_in_name() { + fun `given two relays with same prefix but different numbers comparator should return lowest number first`() { val relay9 = RelayItem.Relay( name = "se9-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relay10 = RelayItem.Relay( name = "se10-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) relay9 assertOrderBothDirection relay10 } @Test - fun test_compare_same_name() { + fun `given two relays with same name with number in name comparator should return 0`() { val relay9a = RelayItem.Relay( name = "se9-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relay9b = RelayItem.Relay( name = "se9-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -55,7 +55,7 @@ class RelayNameComparatorTest { } @Test - fun test_compare_only_numbers_in_name() { + fun `comparator should be able to handle name of only numbers`() { val relay001 = RelayItem.Relay(name = "001", location = mockk(), locationName = "mock", active = false) val relay1 = @@ -72,20 +72,20 @@ class RelayNameComparatorTest { } @Test - fun test_compare_without_numbers_in_name() { + fun `given two relays with same name and without number comparator should return 0`() { val relay9a = RelayItem.Relay( name = "se-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relay9b = RelayItem.Relay( name = "se-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -93,54 +93,54 @@ class RelayNameComparatorTest { } @Test - fun test_compare_with_trailing_zeros_in_name() { + fun `given two relays with leading zeroes comparator should return lowest number first`() { val relay001 = RelayItem.Relay( name = "se001-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relay005 = RelayItem.Relay( name = "se005-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) relay001 assertOrderBothDirection relay005 } @Test - fun test_compare_prefix_and_numbers() { + fun `given 4 relays comparator should sort by prefix then number`() { val relayAr2 = RelayItem.Relay( name = "ar2-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relayAr8 = RelayItem.Relay( name = "ar8-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relaySe5 = RelayItem.Relay( name = "se5-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) val relaySe10 = RelayItem.Relay( name = "se10-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) relayAr2 assertOrderBothDirection relayAr8 @@ -149,7 +149,7 @@ class RelayNameComparatorTest { } @Test - fun test_compare_suffix_and_numbers() { + fun `given two relays with same prefix and number comparator should sort by suffix`() { val relay2c = RelayItem.Relay( name = "se2-cloud", @@ -162,14 +162,14 @@ class RelayNameComparatorTest { name = "se2-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) relay2c assertOrderBothDirection relay2w } @Test - fun test_compare_different_length() { + fun `given two relays with same prefix, but one with no suffix, the one with no suffix should come first`() { val relay22a = RelayItem.Relay( name = "se22", @@ -182,7 +182,7 @@ class RelayNameComparatorTest { name = "se22-wireguard", location = mockk(), locationName = "mock", - active = false + active = false, ) relay22a assertOrderBothDirection relay22b diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt index 4da6c4fdaf..8fd21c5533 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt @@ -50,7 +50,7 @@ class ConnectionProxyTest { } @Test - fun test_initialize_connection_proxy() { + fun `initialize connection proxy should work`() { // Arrange val eventType = slot<KClass<Event.TunnelStateChange>>() every { mockedDispatchingHandler.registerHandler(capture(eventType), any()) } just Runs @@ -60,7 +60,7 @@ class ConnectionProxyTest { } @Test - fun test_tunnel_normal_connect_disconnect() { + fun `normal connect and disconnect should not crash`() { // Arrange every { connection.send(any()) } just Runs every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs @@ -71,7 +71,7 @@ class ConnectionProxyTest { } @Test - fun test_tunnel_handle_crash_on_connect() { + fun `connect should catch DeadObjectException`() { // Arrange every { connection.send(any()) } throws DeadObjectException() every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt index 8f17f37bbd..81b518199c 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt @@ -47,7 +47,7 @@ class ServiceConnectionDeviceDataSourceTest { } @Test - fun test_get_devices_list() { + fun `get device should work`() { // Arrange every { connection.send(any()) } just Runs every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs @@ -58,7 +58,7 @@ class ServiceConnectionDeviceDataSourceTest { } @Test - fun test_catch_exception_on_devices_list() { + fun `get device should catch DeadObjectException`() { // Arrange every { connection.send(any()) } throws DeadObjectException() every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs 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 index f068642bec..39bfae63d8 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt @@ -41,7 +41,7 @@ class AccountExpiryNotificationUseCaseTest { } @Test - fun `ensure notifications are empty by default`() = runTest { + fun `initial state should be empty`() = runTest { // Arrange, Act, Assert accountExpiryNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } @@ -49,7 +49,7 @@ class AccountExpiryNotificationUseCaseTest { } @Test - fun `ensure account expiry within 3 days generates notification`() = runTest { + fun `account that expires within 3 days should emit a notification`() = runTest { // Arrange, Act, Assert accountExpiryNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } @@ -64,7 +64,7 @@ class AccountExpiryNotificationUseCaseTest { } @Test - fun `ensure an expire of 4 days in the future does not produce a notification`() = runTest { + fun `account that expires in 4 days should not emit a notification`() = runTest { // Arrange, Act, Assert accountExpiryNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } 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 index 5e4403c38d..691bb99131 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt @@ -48,25 +48,26 @@ class NewDeviceUseNotificationCaseTest { } @Test - fun `ensure empty by default`() = runTest { + fun `initial state should be empty`() = 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() + fun `when newDeviceCreated is called notifications should emit NewDevice notification containing device name`() = + runTest { + newDeviceNotificationUseCase.notifications().test { + // Arrange, Act + awaitItem() + newDeviceNotificationUseCase.newDeviceCreated() - // Assert - assertEquals(awaitItem(), listOf(InAppNotification.NewDevice(deviceName))) + // Assert + assertEquals(awaitItem(), listOf(InAppNotification.NewDevice(deviceName))) + } } - } @Test - fun `ensure NewDevice notification is cleared`() = runTest { + fun `clearNewDeviceCreatedNotification should clear notifications`() = runTest { newDeviceNotificationUseCase.notifications().test { // Arrange, Act awaitItem() 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 index 6563f7e6e9..d40e8d8e18 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt @@ -36,14 +36,14 @@ class OutOfTimeUseCaseTest { } @Test - fun `No events should result in no expiry`() = runTest { + 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 { + fun `tunnel is blocking because out of time should emit true`() = runTest { // Arrange // Act, Assert val errorStateCause = ErrorStateCause.AuthFailed("[EXPIRED_ACCOUNT]") @@ -58,7 +58,7 @@ class OutOfTimeUseCaseTest { } @Test - fun `Tunnel is connected should emit false`() = runTest { + fun `tunnel is connected should emit false`() = runTest { // Arrange val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1)) val tunnelStateChanges = @@ -86,7 +86,7 @@ class OutOfTimeUseCaseTest { } @Test - fun `Account expiry that has expired should emit true`() = runTest { + fun `account expiry that has expired should emit true`() = runTest { // Arrange val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().minusDays(1)) // Act, Assert @@ -98,7 +98,7 @@ class OutOfTimeUseCaseTest { } @Test - fun `Account expiry that has not expired should emit false`() = runTest { + fun `account expiry that has not expired should emit false`() = runTest { // Arrange val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1)) @@ -111,7 +111,7 @@ class OutOfTimeUseCaseTest { } @Test - fun `Account that expires without new expiry event`() = runTest { + fun `account that expires without new expiry event should emit true`() = runTest { // Arrange val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusSeconds(62)) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt index fb85629a9c..a07fefabe3 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt @@ -21,7 +21,7 @@ class PlayPaymentUseCaseTest { private val playPaymentUseCase = PlayPaymentUseCase(mockPaymentRepository) @Test - fun testUpdatePaymentAvailability() = runTest { + fun `queryPaymentAvailability call should result in updated paymentAvailability`() = runTest { // Arrange val productsUnavailable = PaymentAvailability.ProductsUnavailable val paymentRepositoryQueryPaymentAvailabilityFlow = flow { emit(productsUnavailable) } @@ -37,7 +37,7 @@ class PlayPaymentUseCaseTest { } @Test - fun testUpdatePurchaseResult() = runTest { + fun `purchaseProduct call should result in updated purchaseResult`() = runTest { // Arrange val fetchingProducts = PurchaseResult.FetchingProducts val productId = ProductId("productId") @@ -54,7 +54,7 @@ class PlayPaymentUseCaseTest { } @Test - fun testPurchaseProduct() = runTest { + fun `purchaseProduct call should invoke purchaseProduct on repository`() = runTest { // Arrange val productId = ProductId("productId") @@ -66,16 +66,17 @@ class PlayPaymentUseCaseTest { } @Test - fun testQueryPaymentAvailability() = runTest { - // Act - playPaymentUseCase.queryPaymentAvailability() + fun `queryPaymentAvailability should invoke queryPaymentAvailability on repository`() = + runTest { + // Act + playPaymentUseCase.queryPaymentAvailability() - // Assert - coVerify { mockPaymentRepository.queryPaymentAvailability() } - } + // Assert + coVerify { mockPaymentRepository.queryPaymentAvailability() } + } @Test - fun testResetPurchaseResult() = runTest { + fun `resetPurchaseResult call should result in purchaseResult null`() = runTest { // Arrange val completedSuccess = PurchaseResult.Completed.Success val productId = ProductId("productId") @@ -94,7 +95,7 @@ class PlayPaymentUseCaseTest { } @Test - fun testVerifyPurchases() = runTest { + fun `verifyPurchases call should invoke verifyPurchases on repository`() = runTest { // Act playPaymentUseCase.verifyPurchases() 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 index 244c735ee9..82126099d8 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt @@ -55,13 +55,13 @@ class TunnelStateNotificationUseCaseTest { } @Test - fun `ensure notifications are empty by default`() = runTest { + fun `initial state should be empty`() = runTest { // Arrange, Act, Assert tunnelStateNotificationUseCase.notifications().test { assertTrue { awaitItem().isEmpty() } } } @Test - fun `ensure TunnelState with error will produce TunnelStateError notification`() = runTest { + fun `when TunnelState is error use case should emit TunnelStateError notification`() = runTest { tunnelStateNotificationUseCase.notifications().test { // Arrange, Act assertEquals(emptyList(), awaitItem()) @@ -76,7 +76,7 @@ class TunnelStateNotificationUseCaseTest { } @Test - fun `ensure disconnecting TunnelState with blocking will produce TunnelStateBlocked notification`() = + fun `when TunnelState is Disconnecting with blocking use case should emit TunnelStateBlocked notification`() = runTest { tunnelStateNotificationUseCase.notifications().test { // Arrange, Act 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 index 12f7207280..fbc677b461 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt @@ -69,44 +69,50 @@ class VersionNotificationUseCaseTest { } @Test - fun `ensure notifications are empty by default`() = runTest { + fun `initial state should be empty`() = 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 + fun `when a new version is available use case should emit UpdateAvailable with new version`() = + 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))) + // 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 + fun `when an unsupported version use case should emit UnsupportedVersion notification`() = + 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)) - ) + // 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/utils/VoucherRegexHelperParameterizedTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt index 1722e81a9d..abe325ba43 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt @@ -14,7 +14,7 @@ private const val IS_UNACCEPTED_FORMAT = false @ExtendWith(TestCoroutineRule::class) class VoucherRegexHelperParameterizedTest { - @ParameterizedTest + @ParameterizedTest(name = "Voucher format {1} should be valid: {0}") @MethodSource("data") fun testVoucherFormat(isValid: Boolean, voucher: String) { assertEquals(VoucherRegexHelper.validate(voucher), isValid) 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 637c531039..61758c2d1d 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 @@ -63,7 +63,7 @@ class AccountViewModelTest { private lateinit var viewModel: AccountViewModel @BeforeEach - fun setUp() { + fun setup() { mockkStatic(CACHE_EXTENSION_CLASS) mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS) every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache @@ -88,7 +88,7 @@ class AccountViewModelTest { } @Test - fun testAccountLoggedInState() = runTest { + fun `given device state LoggedIn uiState should contain accountNumber`() = runTest { // Act, Assert viewModel.uiState.test { awaitItem() // Default state @@ -99,7 +99,7 @@ class AccountViewModelTest { } @Test - fun testOnLogoutClick() { + fun `onLogoutClick should invoke logout on AccountRepository`() { // Act viewModel.onLogoutClick() @@ -108,20 +108,21 @@ class AccountViewModelTest { } @Test - fun testBillingProductsUnavailableState() = runTest { - // Arrange in setup + fun `when paymentAvailability emits ProductsUnavailable uiState should be NoPayment`() = + runTest { + // Arrange in setup - // Act, Assert - viewModel.uiState.test { - awaitItem() // Default state - paymentAvailability.tryEmit(PaymentAvailability.ProductsUnavailable) - val result = awaitItem().billingPaymentState - assertIs<PaymentState.NoPayment>(result) + // Act, Assert + viewModel.uiState.test { + awaitItem() // Default state + paymentAvailability.tryEmit(PaymentAvailability.ProductsUnavailable) + val result = awaitItem().billingPaymentState + assertIs<PaymentState.NoPayment>(result) + } } - } @Test - fun testBillingProductsGenericErrorState() = runTest { + fun `when paymentAvailability emits ErrorOther uiState should be ErrorGeneric`() = runTest { // Act, Assert viewModel.uiState.test { awaitItem() // Default state @@ -132,34 +133,38 @@ class AccountViewModelTest { } @Test - fun testBillingProductsBillingErrorState() = runTest { - // Act, Assert - viewModel.uiState.test { - awaitItem() // Default state - paymentAvailability.tryEmit(PaymentAvailability.Error.BillingUnavailable) - val result = awaitItem().billingPaymentState - assertIs<PaymentState.Error.Billing>(result) + fun `when paymentAvailability emits ErrorBillingUnavailable uiState should be ErrorBilling`() = + runTest { + // Act, Assert + viewModel.uiState.test { + awaitItem() // Default state + paymentAvailability.tryEmit(PaymentAvailability.Error.BillingUnavailable) + val result = awaitItem().billingPaymentState + assertIs<PaymentState.Error.Billing>(result) + } } - } @Test - fun testBillingProductsPaymentAvailableState() = runTest { - // Arrange - val mockProduct: PaymentProduct = mockk() - val expectedProductList = listOf(mockProduct) + fun `when paymentAvailability emits ProductsAvailable uiState should be Available with products`() = + runTest { + // Arrange + val mockProduct: PaymentProduct = mockk() + val expectedProductList = listOf(mockProduct) - // Act, Assert - viewModel.uiState.test { - awaitItem() // Default state - paymentAvailability.tryEmit(PaymentAvailability.ProductsAvailable(listOf(mockProduct))) - val result = awaitItem().billingPaymentState - assertIs<PaymentState.PaymentAvailable>(result) - assertLists(expectedProductList, result.products) + // Act, Assert + viewModel.uiState.test { + awaitItem() // Default state + paymentAvailability.tryEmit( + PaymentAvailability.ProductsAvailable(listOf(mockProduct)) + ) + val result = awaitItem().billingPaymentState + assertIs<PaymentState.PaymentAvailable>(result) + assertLists(expectedProductList, result.products) + } } - } @Test - fun testStartBillingPayment() { + fun `startBillingPayment should invoke purchaseProduct on PaymentUseCase`() { // Arrange val mockProductId = ProductId("MOCK") val mockActivityProvider = mockk<() -> Activity>() @@ -172,7 +177,7 @@ class AccountViewModelTest { } @Test - fun testOnClosePurchaseResultDialogSuccessful() { + fun `onClosePurchaseResultDialog with success should invoke fetchAccountExpiry on AccountRepository`() { // Arrange // Act @@ -180,11 +185,21 @@ class AccountViewModelTest { // Assert verify { mockAccountRepository.fetchAccountExpiry() } + } + + @Test + fun `onClosePurchaseResultDialog with success should invoke resetPurchaseResult on PaymentUseCase`() { + // Arrange + + // Act + viewModel.onClosePurchaseResultDialog(success = true) + + // Assert coVerify { mockPaymentUseCase.resetPurchaseResult() } } @Test - fun testOnClosePurchaseResultDialogNotSuccessful() { + fun `onClosePurchaseResultDialog with success false should invoke queryPaymentAvailability on PaymentUseCase`() { // Arrange // Act @@ -192,6 +207,16 @@ class AccountViewModelTest { // Assert coVerify { mockPaymentUseCase.queryPaymentAvailability() } + } + + @Test + fun `onClosePurchaseResultDialog with success false should invoke resetPurchaseResult on PaymentUseCase`() { + // Arrange + + // Act + viewModel.onClosePurchaseResultDialog(success = false) + + // Assert coVerify { mockPaymentUseCase.resetPurchaseResult() } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt index d9a5ae534e..46126f5ad8 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt @@ -8,8 +8,9 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockkStatic import io.mockk.unmockkAll -import kotlin.test.assertNotNull +import kotlin.test.assertEquals import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.repository.ChangelogRepository import org.junit.jupiter.api.AfterEach @@ -38,7 +39,7 @@ class ChangelogViewModelTest { } @Test - fun testUpToDateVersionCodeShouldNotEmitChangelog() = runTest { + fun `given up to date version code uiSideEffect should not emit`() = runTest { // Arrange every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns buildVersionCode @@ -49,18 +50,26 @@ class ChangelogViewModelTest { } @Test - fun testNotUpToDateVersionCodeShouldEmitChangelog() = runTest { + fun `given old version code uiSideEffect should emit ChangeLog`() = runTest { // Arrange - every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns -1 - every { mockedChangelogRepository.getLastVersionChanges() } returns listOf("bla", "bla") + val version = -1 + val changes = listOf("first change", "second change") + every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns + version + every { mockedChangelogRepository.getLastVersionChanges() } returns changes viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersionCode, false) // Given a new version with a change log we should return it - viewModel.uiSideEffect.test { assertNotNull(awaitItem()) } + viewModel.uiSideEffect.test { + assertEquals( + awaitItem(), + Changelog(version = BuildConfig.VERSION_NAME, changes = changes) + ) + } } @Test - fun testEmptyChangelogShouldNotEmitChangelog() = runTest { + fun `given old version code and empty change log uiSideEffect should not emit`() = runTest { // Arrange every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns -1 every { mockedChangelogRepository.getLastVersionChanges() } returns emptyList() 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 05dae1d53d..545422b6f2 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 @@ -154,12 +154,12 @@ class ConnectViewModelTest { } @Test - fun testInitialState() = runTest { + fun `uiState should emit initial state by default`() = runTest { viewModel.uiState.test { assertEquals(ConnectUiState.INITIAL, awaitItem()) } } @Test - fun testTunnelRealStateUpdate() = runTest { + fun `given change in tunnelRealState uiState should emit new tunnelRealState`() = runTest { val tunnelRealStateTestItem = TunnelState.Connected(mockk(relaxed = true), null) viewModel.uiState.test { @@ -173,7 +173,7 @@ class ConnectViewModelTest { } @Test - fun testTunnelUiStateUpdate() = runTest { + fun `given change in tunnelUiState uiState should emit new tunnelUiState`() = runTest { val tunnelUiStateTestItem = TunnelState.Connected(mockk(), mockk()) viewModel.uiState.test { @@ -187,22 +187,28 @@ class ConnectViewModelTest { } @Test - fun testSelectedLocationUpdate() = runTest { - val selectedRelayItem = - RelayItem.Country(name = "Name", code = "Code", expanded = false, cities = emptyList()) - selectedRelayItemFlow.value = selectedRelayItem + fun `given RelayListUseCase returns new selectedRelayItem uiState should emit new selectedRelayItem`() = + runTest { + val selectedRelayItem = + RelayItem.Country( + name = "Name", + code = "Code", + expanded = false, + cities = emptyList() + ) + selectedRelayItemFlow.value = selectedRelayItem - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - val result = awaitItem() - assertEquals(selectedRelayItem, result.selectedRelayItem) + viewModel.uiState.test { + assertEquals(ConnectUiState.INITIAL, awaitItem()) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + val result = awaitItem() + assertEquals(selectedRelayItem, result.selectedRelayItem) + } } - } @Test - fun testLocationUpdate() = runTest { + fun `given new location in tunnel state uiState should emit new location`() = runTest { val locationTestItem = GeoIpLocation( ipv4 = mockk(relaxed = true), @@ -231,7 +237,7 @@ class ConnectViewModelTest { } @Test - fun testLocationUpdateNullLocation() = + fun `initial state should not include any location`() = // Arrange runTest { val locationTestItem = null @@ -248,7 +254,7 @@ class ConnectViewModelTest { } @Test - fun testOnDisconnectClick() = runTest { + fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest { val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true) every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy viewModel.onDisconnectClick() @@ -256,7 +262,7 @@ class ConnectViewModelTest { } @Test - fun testOnReconnectClick() = runTest { + fun `onReconnectClick should invoke reconnect on ConnectionProxy`() = runTest { val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true) every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy viewModel.onReconnectClick() @@ -264,7 +270,7 @@ class ConnectViewModelTest { } @Test - fun testOnConnectClick() = runTest { + fun `onConnectClick should invoke connect on ConnectionProxy`() = runTest { val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true) every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy viewModel.onConnectClick() @@ -272,7 +278,7 @@ class ConnectViewModelTest { } @Test - fun testOnCancelClick() = runTest { + fun `onCancelClick should invoke disconnect on ConnectionProxy`() = runTest { val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true) every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy viewModel.onCancelClick() @@ -280,43 +286,46 @@ class ConnectViewModelTest { } @Test - fun testErrorNotificationState() = runTest { - // Arrange - val mockErrorState: ErrorState = mockk() - val expectedConnectNotificationState = InAppNotification.TunnelStateError(mockErrorState) - val tunnelUiState = TunnelState.Error(mockErrorState) - notifications.value = listOf(expectedConnectNotificationState) + fun `given InAppNotificationController returns TunnelStateError notification uiState should emit notification`() = + runTest { + // Arrange + val mockErrorState: ErrorState = mockk() + val expectedConnectNotificationState = + InAppNotification.TunnelStateError(mockErrorState) + val tunnelUiState = TunnelState.Error(mockErrorState) + notifications.value = listOf(expectedConnectNotificationState) - // Act, Assert - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - eventNotifierTunnelUiState.notify(tunnelUiState) - val result = awaitItem() - assertEquals(expectedConnectNotificationState, result.inAppNotification) + // Act, Assert + viewModel.uiState.test { + assertEquals(ConnectUiState.INITIAL, awaitItem()) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + eventNotifierTunnelUiState.notify(tunnelUiState) + val result = awaitItem() + assertEquals(expectedConnectNotificationState, result.inAppNotification) + } } - } @Test - fun testOnShowAccountClick() = runTest { - // Arrange - val mockToken = "4444 5555 6666 7777" - val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true) - every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache - coEvery { mockAuthTokenCache.fetchAuthToken() } returns mockToken + fun `onShowAccountClick call should result in uiSideEffect emitting OpenAccountManagementPageInBrowser`() = + runTest { + // Arrange + val mockToken = "4444 5555 6666 7777" + val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true) + every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache + coEvery { mockAuthTokenCache.fetchAuthToken() } returns mockToken - // Act, Assert - viewModel.uiSideEffect.test { - viewModel.onManageAccountClick() - val action = awaitItem() - assertIs<ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser>(action) - assertEquals(mockToken, action.token) + // Act, Assert + viewModel.uiSideEffect.test { + viewModel.onManageAccountClick() + val action = awaitItem() + assertIs<ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser>(action) + assertEquals(mockToken, action.token) + } } - } @Test - fun testOutOfTimeUiSideEffect() = runTest { + fun `given OutOfTimeUseCase returns true uiSideEffect should emit OutOfTime`() = runTest { // Arrange val deferred = async { viewModel.uiSideEffect.first() } 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 3f7966e8bd..11244e9df4 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 @@ -61,7 +61,7 @@ class DeviceRevokedViewModelTest { } @Test - fun testUiStateWhenServiceNotConnected() = runTest { + fun `when service connection is Disconnected then uiState should be UNKNOWN`() = runTest { // Arrange, Act, Assert viewModel.uiState.test { serviceConnectionState.value = ServiceConnectionState.Disconnected @@ -70,7 +70,7 @@ class DeviceRevokedViewModelTest { } @Test - fun testUiStateWhenServiceConnectedButNotReady() = runTest { + fun `when service connection is ConnectedNotReady then uiState should be UNKNOWN`() = runTest { // Arrange, Act, Assert viewModel.uiState.test { serviceConnectionState.value = ServiceConnectionState.ConnectedNotReady(mockk()) @@ -79,7 +79,7 @@ class DeviceRevokedViewModelTest { } @Test - fun testUiStateWhenServiceConnectedAndReady() = runTest { + fun `when service connection is ConnectedReady uiState should be SECURED`() = runTest { // Arrange val mockedContainer = mockk<ServiceConnectionContainer>().apply { @@ -104,7 +104,7 @@ class DeviceRevokedViewModelTest { } @Test - fun testGoToLoginWhenDisconnected() { + fun `onGoToLoginClicked should invoke logout on AccountRepository`() { // Arrange val mockedContainer = mockk<ServiceConnectionContainer>().also { @@ -122,7 +122,7 @@ class DeviceRevokedViewModelTest { } @Test - fun testGoToLoginWhenConnected() { + fun `onGoToLoginClicked should invoke disconnect before logout when connected`() { // Arrange val mockedContainer = mockk<ServiceConnectionContainer>().also { diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt index 210ed88666..fda88bff79 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt @@ -71,59 +71,63 @@ class FilterViewModelTest { } @Test - fun testSetSelectedOwnership() = runTest { - // Arrange - val mockOwnership = Ownership.Rented - // Assert - viewModel.uiState.test { - assertEquals(awaitItem().selectedOwnership, Ownership.MullvadOwned) - viewModel.setSelectedOwnership(mockOwnership) - assertEquals(mockOwnership, awaitItem().selectedOwnership) + fun `setSelectedOwnership with Rented should emit uiState where selectedOwnership is Rented`() = + runTest { + // Arrange + val mockOwnership = Ownership.Rented + // Assert + viewModel.uiState.test { + assertEquals(awaitItem().selectedOwnership, Ownership.MullvadOwned) + viewModel.setSelectedOwnership(mockOwnership) + assertEquals(mockOwnership, awaitItem().selectedOwnership) + } } - } @Test - fun testSetSelectedProvider() = runTest { - // Arrange - val mockSelectedProvidersList = Provider("ptisp", false) - // Assert - viewModel.uiState.test { - assertLists(awaitItem().selectedProviders, mockSelectedProviders) - viewModel.setSelectedProvider(true, mockSelectedProvidersList) - assertLists( - listOf(mockSelectedProvidersList) + mockSelectedProviders, - awaitItem().selectedProviders - ) + fun `setSelectionProvider should emit uiState where selectedProviders include the selected provider`() = + runTest { + // Arrange + val mockSelectedProvidersList = Provider("ptisp", false) + // Assert + viewModel.uiState.test { + assertLists(awaitItem().selectedProviders, mockSelectedProviders) + viewModel.setSelectedProvider(true, mockSelectedProvidersList) + assertLists( + listOf(mockSelectedProvidersList) + mockSelectedProviders, + awaitItem().selectedProviders + ) + } } - } @Test - fun testSetAllProviders() = runTest { - // Arrange - val mockProvidersList = dummyListOfAllProviders - // Act - viewModel.setAllProviders(true) - // Assert - viewModel.uiState.test { - val state = awaitItem() - assertEquals(mockProvidersList, state.selectedProviders) + fun `setAllProvider with true should emit uiState with selectedProviders includes all providers`() = + runTest { + // Arrange + val mockProvidersList = dummyListOfAllProviders + // Act + viewModel.setAllProviders(true) + // Assert + viewModel.uiState.test { + val state = awaitItem() + assertEquals(mockProvidersList, state.selectedProviders) + } } - } @Test - fun testOnApplyButtonClicked() = runTest { - // Arrange - val mockOwnership = Ownership.MullvadOwned.toOwnershipConstraint() - val mockSelectedProviders = - mockSelectedProviders.toConstraintProviders(dummyListOfAllProviders) - // Act - viewModel.onApplyButtonClicked() - // Assert - coVerify { - mockRelayListFilterUseCase.updateOwnershipAndProviderFilter( - mockOwnership, - mockSelectedProviders - ) + fun `onApplyButtonClicked should invoke updateOwnershipAndProviderFilter on RelayListFilterUseCase`() = + runTest { + // Arrange + val mockOwnership = Ownership.MullvadOwned.toOwnershipConstraint() + val mockSelectedProviders = + mockSelectedProviders.toConstraintProviders(dummyListOfAllProviders) + // Act + viewModel.onApplyButtonClicked() + // Assert + coVerify { + mockRelayListFilterUseCase.updateOwnershipAndProviderFilter( + mockOwnership, + mockSelectedProviders + ) + } } - } } 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 f677615c24..3271fe57eb 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 @@ -66,7 +66,7 @@ class LoginViewModelTest { } @Test - fun testIsInternetAvailableWithoutInternet() = runTest { + fun `given no internet when logging in then show no internet error`() = runTest { turbineScope { // Arrange every { connectivityUseCase.isInternetAvailable() } returns false @@ -87,12 +87,12 @@ class LoginViewModelTest { } @Test - fun testDefaultState() = runTest { + fun `initial state should be initial`() = runTest { loginViewModel.uiState.test { assertEquals(LoginUiState.INITIAL, awaitItem()) } } @Test - fun testCreateAccount() = runTest { + fun `createAccount call should result in NavigateToWelcome side effect`() = runTest { turbineScope { // Arrange val uiStates = loginViewModel.uiState.testIn(backgroundScope) @@ -109,7 +109,7 @@ class LoginViewModelTest { } @Test - fun testLoginWithValidAccount() = runTest { + fun `given valid account when logging in then navigate to connect view`() = runTest { turbineScope { // Arrange val uiStates = loginViewModel.uiState.testIn(backgroundScope) @@ -128,7 +128,7 @@ class LoginViewModelTest { } @Test - fun testLoginWithInvalidAccount() = runTest { + fun `given invalid account when logging in then show invalid credentials`() = runTest { loginViewModel.uiState.test { // Arrange coEvery { mockedAccountRepository.login(any()) } returns LoginResult.InvalidAccount @@ -142,34 +142,36 @@ class LoginViewModelTest { } @Test - fun testLoginWithTooManyDevicesError() = runTest { - turbineScope { - // Arrange - val uiStates = loginViewModel.uiState.testIn(backgroundScope) - val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope) - coEvery { - mockedDeviceRepository.refreshAndAwaitDeviceListWithTimeout( - any(), - any(), - any(), - any() - ) - } returns DeviceListEvent.Available(DUMMY_ACCOUNT_TOKEN, listOf()) - coEvery { mockedAccountRepository.login(any()) } returns LoginResult.MaxDevicesReached + fun `given account with max devices reached when logging devices reached then navigate to too many devices`() = + runTest { + turbineScope { + // Arrange + val uiStates = loginViewModel.uiState.testIn(backgroundScope) + val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope) + coEvery { + mockedDeviceRepository.refreshAndAwaitDeviceListWithTimeout( + any(), + any(), + any(), + any() + ) + } returns DeviceListEvent.Available(DUMMY_ACCOUNT_TOKEN, listOf()) + coEvery { mockedAccountRepository.login(any()) } returns + LoginResult.MaxDevicesReached - // Act, Assert - uiStates.skipDefaultItem() - loginViewModel.login(DUMMY_ACCOUNT_TOKEN) - assertEquals(Loading.LoggingIn, uiStates.awaitItem().loginState) - assertEquals( - LoginUiSideEffect.TooManyDevices(AccountToken(DUMMY_ACCOUNT_TOKEN)), - sideEffects.awaitItem() - ) + // Act, Assert + uiStates.skipDefaultItem() + loginViewModel.login(DUMMY_ACCOUNT_TOKEN) + assertEquals(Loading.LoggingIn, uiStates.awaitItem().loginState) + assertEquals( + LoginUiSideEffect.TooManyDevices(AccountToken(DUMMY_ACCOUNT_TOKEN)), + sideEffects.awaitItem() + ) + } } - } @Test - fun testLoginWithRpcError() = runTest { + fun `given RpcError when logging in then show unknown error with message`() = runTest { loginViewModel.uiState.test { // Arrange coEvery { mockedAccountRepository.login(any()) } returns LoginResult.RpcError @@ -186,7 +188,7 @@ class LoginViewModelTest { } @Test - fun testLoginWithUnknownError() = runTest { + fun `given OtherError when logging in then show unknown error with message`() = runTest { loginViewModel.uiState.test { // Arrange coEvery { mockedAccountRepository.login(any()) } returns LoginResult.OtherError @@ -203,20 +205,21 @@ class LoginViewModelTest { } @Test - fun testAccountHistory() = runTest { - loginViewModel.uiState.test { - // Act, Assert - skipDefaultItem() - accountHistoryTestEvents.emit(AccountHistory.Available(DUMMY_ACCOUNT_TOKEN)) - assertEquals( - LoginUiState.INITIAL.copy(lastUsedAccount = AccountToken(DUMMY_ACCOUNT_TOKEN)), - awaitItem() - ) + fun `on new accountHistory emission uiState should include lastUsedAccount matching accountHistory`() = + runTest { + loginViewModel.uiState.test { + // Act, Assert + skipDefaultItem() + accountHistoryTestEvents.emit(AccountHistory.Available(DUMMY_ACCOUNT_TOKEN)) + assertEquals( + LoginUiState.INITIAL.copy(lastUsedAccount = AccountToken(DUMMY_ACCOUNT_TOKEN)), + awaitItem() + ) + } } - } @Test - fun testClearingAccountHistory() = runTest { + fun `clearAccountHistory should invoke clearAccountHistory on AccountRepository`() = runTest { // Act, Assert loginViewModel.clearAccountHistory() verify { 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 0df34e0747..a5171b2ea6 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 @@ -71,7 +71,7 @@ class OutOfTimeViewModelTest { private lateinit var viewModel: OutOfTimeViewModel @BeforeEach - fun setUp() { + fun setup() { mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS) mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS) @@ -110,7 +110,7 @@ class OutOfTimeViewModelTest { } @Test - fun testSitePaymentClick() = runTest { + fun `when clicking on site payment then open website account view`() = runTest { // Arrange val mockToken = "4444 5555 6666 7777" val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true) @@ -127,7 +127,7 @@ class OutOfTimeViewModelTest { } @Test - fun testUpdateTunnelState() = runTest { + fun `when tunnel state changes then ui should be updated`() = runTest { // Arrange val tunnelRealStateTestItem = TunnelState.Connected(mockk(), mockk()) @@ -143,21 +143,22 @@ class OutOfTimeViewModelTest { } @Test - fun testOpenConnectScreen() = runTest { - // Arrange - val mockExpiryDate: DateTime = mockk() - every { mockExpiryDate.isAfter(any<ReadableInstant>()) } returns true + fun `when OutOfTimeUseCase returns false uiSideEffect should emit OpenConnectScreen`() = + runTest { + // Arrange + val mockExpiryDate: DateTime = mockk() + every { mockExpiryDate.isAfter(any<ReadableInstant>()) } returns true - // Act, Assert - viewModel.uiSideEffect.test { - outOfTimeFlow.value = false - val action = awaitItem() - assertIs<OutOfTimeViewModel.UiSideEffect.OpenConnectScreen>(action) + // Act, Assert + viewModel.uiSideEffect.test { + outOfTimeFlow.value = false + val action = awaitItem() + assertIs<OutOfTimeViewModel.UiSideEffect.OpenConnectScreen>(action) + } } - } @Test - fun testOnDisconnectClick() = runTest { + fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest { // Arrange val mockProxy: ConnectionProxy = mockk(relaxed = true) every { mockServiceConnectionManager.connectionProxy() } returns mockProxy @@ -170,70 +171,74 @@ class OutOfTimeViewModelTest { } @Test - fun testBillingProductsUnavailableState() = runTest { - // Arrange - val productsUnavailable = PaymentAvailability.ProductsUnavailable - paymentAvailabilityFlow.value = productsUnavailable - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ProductsUnavailable uiState should include state NoPayment`() = + runTest { + // Arrange + val productsUnavailable = PaymentAvailability.ProductsUnavailable + paymentAvailabilityFlow.value = productsUnavailable + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.NoPayment>(result) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.NoPayment>(result) + } } - } @Test - fun testBillingProductsGenericErrorState() = runTest { - // Arrange - val paymentAvailabilityError = PaymentAvailability.Error.Other(mockk()) - paymentAvailabilityFlow.value = paymentAvailabilityError - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ErrorOther uiState should include state ErrorGeneric`() = + runTest { + // Arrange + val paymentAvailabilityError = PaymentAvailability.Error.Other(mockk()) + paymentAvailabilityFlow.value = paymentAvailabilityError + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.Error.Generic>(result) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.Error.Generic>(result) + } } - } @Test - fun testBillingProductsBillingErrorState() = runTest { - // Arrange - val paymentAvailabilityError = PaymentAvailability.Error.BillingUnavailable - paymentAvailabilityFlow.value = paymentAvailabilityError - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ErrorBillingUnavailable uiState should be ErrorBilling`() = + runTest { + // Arrange + val paymentAvailabilityError = PaymentAvailability.Error.BillingUnavailable + paymentAvailabilityFlow.value = paymentAvailabilityError + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.Error.Billing>(result) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.Error.Billing>(result) + } } - } @Test - fun testBillingProductsPaymentAvailableState() = runTest { - // Arrange - val mockProduct: PaymentProduct = mockk() - val expectedProductList = listOf(mockProduct) - val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct)) - paymentAvailabilityFlow.value = productsAvailable - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ProductsAvailable uiState should be Available with products`() = + runTest { + // Arrange + val mockProduct: PaymentProduct = mockk() + val expectedProductList = listOf(mockProduct) + val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct)) + paymentAvailabilityFlow.value = productsAvailable + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.PaymentAvailable>(result) - assertLists(expectedProductList, result.products) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.PaymentAvailable>(result) + assertLists(expectedProductList, result.products) + } } - } @Test - fun testOnClosePurchaseResultDialogSuccessful() { + fun `onClosePurchaseResultDialog with success should invoke fetchAccountExpiry on AccountRepository`() { // Arrange // Act @@ -241,11 +246,21 @@ class OutOfTimeViewModelTest { // Assert verify { mockAccountRepository.fetchAccountExpiry() } + } + + @Test + fun `onClosePurchaseResultDialog with success should invoke resetPurchaseResult on PaymentUseCase`() { + // Arrange + + // Act + viewModel.onClosePurchaseResultDialog(success = true) + + // Assert coVerify { mockPaymentUseCase.resetPurchaseResult() } } @Test - fun testOnClosePurchaseResultDialogNotSuccessful() { + fun `onClosePurchaseResultDialog with success false should invoke queryPaymentAvailability on PaymentUseCase`() { // Arrange // Act @@ -253,6 +268,16 @@ class OutOfTimeViewModelTest { // Assert coVerify { mockPaymentUseCase.queryPaymentAvailability() } + } + + @Test + fun `onClosePurchaseResultDialog with success false should invoke resetPurchaseResult on PaymentUseCase`() { + // Arrange + + // Act + viewModel.onClosePurchaseResultDialog(success = false) + + // Assert coVerify { mockPaymentUseCase.resetPurchaseResult() } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/PaymentViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/PaymentViewModelTest.kt index 1e2fba3c96..49e98c95e6 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/PaymentViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/PaymentViewModelTest.kt @@ -27,7 +27,7 @@ class PaymentViewModelTest { private lateinit var viewModel: PaymentViewModel @BeforeEach - fun setUp() { + fun setup() { coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResult viewModel = PaymentViewModel(paymentUseCase = mockPaymentUseCase) @@ -39,24 +39,25 @@ class PaymentViewModelTest { } @Test - fun testBillingUserCancelled() = runTest { - // Arrange - val result = PurchaseResult.Completed.Cancelled - purchaseResult.value = result - - // Act, Assert - viewModel.uiState.test { - assertEquals(PaymentDialogData(), awaitItem().paymentDialogData) + fun `given PaymentUseCase purchaseResult emits cancelled uiSideEffect should emit PaymentCancelled`() = + runTest { + // Arrange + val result = PurchaseResult.Completed.Cancelled purchaseResult.value = result - } - viewModel.uiSideEffect.test { - assertEquals(PaymentUiSideEffect.PaymentCancelled, awaitItem()) + // Act, Assert + viewModel.uiState.test { + assertEquals(PaymentDialogData(), awaitItem().paymentDialogData) + purchaseResult.value = result + } + + viewModel.uiSideEffect.test { + assertEquals(PaymentUiSideEffect.PaymentCancelled, awaitItem()) + } } - } @Test - fun testBillingPurchaseSuccess() = runTest { + fun `purchaseResult emitting Success should result in success dialog state`() = runTest { // Arrange val result = PurchaseResult.Completed.Success diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt index 6ea1a85ed2..80cb2d5e58 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt @@ -32,7 +32,7 @@ class ReportProblemViewModelTest { private lateinit var viewModel: ReportProblemViewModel @BeforeEach - fun setUp() { + fun setup() { MockKAnnotations.init(this) coEvery { mockMullvadProblemReport.collectLogs() } returns true coEvery { mockProblemReportRepository.problemReport } returns problemReportFlow @@ -45,129 +45,135 @@ class ReportProblemViewModelTest { } @Test - fun sendReportFailedToCollectLogs() = runTest { - // Arrange - coEvery { mockMullvadProblemReport.sendReport(any()) } returns - SendProblemReportResult.Error.CollectLog - val email = "my@email.com" + fun `when sendReport returns CollectLog error then uiState should emit sendingState with CollectLog error`() = + runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Error.CollectLog + val email = "my@email.com" - // Act, Assert - viewModel.uiState.test { - assertEquals(null, awaitItem().sendingState) - viewModel.sendReport(email, "My description") - assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) - assertEquals( - SendingReportUiState.Error(SendProblemReportResult.Error.CollectLog), - awaitItem().sendingState - ) + // Act, Assert + viewModel.uiState.test { + assertEquals(null, awaitItem().sendingState) + viewModel.sendReport(email, "My description") + assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) + assertEquals( + SendingReportUiState.Error(SendProblemReportResult.Error.CollectLog), + awaitItem().sendingState + ) + } } - } @Test - fun sendReportFailedToSendReport() = runTest { - // Arrange - coEvery { mockMullvadProblemReport.sendReport(any()) } returns - SendProblemReportResult.Error.SendReport - val email = "my@email.com" + fun `when sendReport returns SendReport error then uiState should emit sendingState with SendReport error`() = + runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Error.SendReport + val email = "my@email.com" - // Act, Assert - viewModel.uiState.test { - assertEquals(null, awaitItem().sendingState) - viewModel.sendReport(email, "My description") - assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) - assertEquals( - SendingReportUiState.Error(SendProblemReportResult.Error.SendReport), - awaitItem().sendingState - ) + // Act, Assert + viewModel.uiState.test { + assertEquals(null, awaitItem().sendingState) + viewModel.sendReport(email, "My description") + assertEquals(SendingReportUiState.Sending, awaitItem().sendingState) + assertEquals( + SendingReportUiState.Error(SendProblemReportResult.Error.SendReport), + awaitItem().sendingState + ) + } } - } @Test - fun sendReportWithoutEmailSuccessfully() = runTest { - // Arrange - coEvery { mockMullvadProblemReport.sendReport(any()) } returns - SendProblemReportResult.Success - val email = "" - val description = "My description" + fun `when sendReport with no email returns Success then uiState should emit sendingState with Success`() = + runTest { + // Arrange + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Success + val email = "" + val description = "My description" - coEvery { mockProblemReportRepository.setDescription(any()) } answers - { - problemReportFlow.value = problemReportFlow.value.copy(description = arg(0)) - } + coEvery { mockProblemReportRepository.setDescription(any()) } answers + { + problemReportFlow.value = problemReportFlow.value.copy(description = arg(0)) + } - // Act, Assert - viewModel.uiState.test { - assertEquals(ReportProblemUiState(), awaitItem()) - viewModel.updateDescription(description) - assertEquals(ReportProblemUiState(description = description), awaitItem()) + // Act, Assert + viewModel.uiState.test { + assertEquals(ReportProblemUiState(), awaitItem()) + viewModel.updateDescription(description) + assertEquals(ReportProblemUiState(description = description), awaitItem()) - viewModel.sendReport(email, description, true) - assertEquals( - ReportProblemUiState(SendingReportUiState.Sending, email, description), - awaitItem() - ) - assertEquals( - ReportProblemUiState( - SendingReportUiState.Success(null), - "", - "", - ), - awaitItem() - ) + viewModel.sendReport(email, description, true) + assertEquals( + ReportProblemUiState(SendingReportUiState.Sending, email, description), + awaitItem() + ) + assertEquals( + ReportProblemUiState( + SendingReportUiState.Success(null), + "", + "", + ), + awaitItem() + ) + } } - } @Test - fun sendReportSuccessfully() = runTest { - // Arrange - coEvery { mockMullvadProblemReport.collectLogs() } returns true - coEvery { mockMullvadProblemReport.sendReport(any()) } returns - SendProblemReportResult.Success - val email = "my@email.com" - val description = "My description" + fun `when sendReport with email returns Success then uiState should emit sendingState with Success`() = + runTest { + // Arrange + coEvery { mockMullvadProblemReport.collectLogs() } returns true + coEvery { mockMullvadProblemReport.sendReport(any()) } returns + SendProblemReportResult.Success + val email = "my@email.com" + val description = "My description" - // This might look a bit weird, and is not optimal. An alternative would be to use the real - // ProblemReportRepository, but that would complicate the other tests. This is a compromise. - coEvery { mockProblemReportRepository.setEmail(any()) } answers - { - problemReportFlow.value = problemReportFlow.value.copy(email = arg(0)) - } - coEvery { mockProblemReportRepository.setDescription(any()) } answers - { - problemReportFlow.value = problemReportFlow.value.copy(description = arg(0)) - } + // This might look a bit weird, and is not optimal. An alternative would be to use the + // real + // ProblemReportRepository, but that would complicate the other tests. This is a + // compromise. + coEvery { mockProblemReportRepository.setEmail(any()) } answers + { + problemReportFlow.value = problemReportFlow.value.copy(email = arg(0)) + } + coEvery { mockProblemReportRepository.setDescription(any()) } answers + { + problemReportFlow.value = problemReportFlow.value.copy(description = arg(0)) + } - // Act, Assert - viewModel.uiState.test { - assertEquals(awaitItem(), ReportProblemUiState(null, "", "")) - viewModel.updateEmail(email) - awaitItem() - viewModel.updateDescription(description) - awaitItem() - - viewModel.sendReport(email, description) - - assertEquals( - ReportProblemUiState( - SendingReportUiState.Sending, - email, - description, - ), + // Act, Assert + viewModel.uiState.test { + assertEquals(awaitItem(), ReportProblemUiState(null, "", "")) + viewModel.updateEmail(email) awaitItem() - ) - assertEquals( - ReportProblemUiState( - SendingReportUiState.Success(email), - "", - "", - ), + viewModel.updateDescription(description) awaitItem() - ) + + viewModel.sendReport(email, description) + + assertEquals( + ReportProblemUiState( + SendingReportUiState.Sending, + email, + description, + ), + awaitItem() + ) + assertEquals( + ReportProblemUiState( + SendingReportUiState.Success(email), + "", + "", + ), + awaitItem() + ) + } } - } @Test - fun testUpdateEmail() = runTest { + fun `updateEmail should invoke setEmail on ProblemReportRepository`() = runTest { // Arrange val email = "my@email.com" @@ -179,7 +185,7 @@ class ReportProblemViewModelTest { } @Test - fun testUpdateDescription() = runTest { + fun `updateDescription should invoke updateDescription on ProblemReportRepository`() = runTest { // Arrange val description = "My description" diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt index 4224563d3b..d8bcc7080f 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt @@ -78,12 +78,12 @@ class SelectLocationViewModelTest { } @Test - fun testInitialState() = runTest { + fun `initial state should be loading`() = runTest { assertEquals(SelectLocationUiState.Loading, viewModel.uiState.value) } @Test - fun testUpdateLocations() = runTest { + fun `given relayListWithSelection emits update uiState should contain new update`() = runTest { // Arrange val mockCountries = listOf<RelayItem.Country>(mockk(), mockk()) val mockCustomList = listOf<RelayItem.CustomList>(mockk()) @@ -108,32 +108,34 @@ class SelectLocationViewModelTest { } @Test - fun testUpdateLocationsNoSelectedRelay() = runTest { - // Arrange - val mockCustomList = listOf<RelayItem.CustomList>(mockk()) - val mockCountries = listOf<RelayItem.Country>(mockk(), mockk()) - val selectedItem: RelayItem? = null - every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries - relayListWithSelectionFlow.value = RelayList(mockCustomList, mockCountries, selectedItem) + fun `given relayListWithSelection emits update with no selections selectedItem should be null`() = + runTest { + // Arrange + val mockCustomList = listOf<RelayItem.CustomList>(mockk()) + val mockCountries = listOf<RelayItem.Country>(mockk(), mockk()) + val selectedItem: RelayItem? = null + every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries + relayListWithSelectionFlow.value = + RelayList(mockCustomList, mockCountries, selectedItem) - // Act, Assert - viewModel.uiState.test { - val actualState = awaitItem() - assertIs<SelectLocationUiState.Data>(actualState) - assertIs<RelayListState.RelayList>(actualState.relayListState) - assertLists( - mockCountries, - (actualState.relayListState as RelayListState.RelayList).countries - ) - assertEquals( - selectedItem, - (actualState.relayListState as RelayListState.RelayList).selectedItem - ) + // Act, Assert + viewModel.uiState.test { + val actualState = awaitItem() + assertIs<SelectLocationUiState.Data>(actualState) + assertIs<RelayListState.RelayList>(actualState.relayListState) + assertLists( + mockCountries, + (actualState.relayListState as RelayListState.RelayList).countries + ) + assertEquals( + selectedItem, + (actualState.relayListState as RelayListState.RelayList).selectedItem + ) + } } - } @Test - fun testSelectRelayAndClose() = runTest { + fun `on selectRelay call uiSideEffect should emit CloseScreen and connect`() = runTest { // Arrange val mockRelayItem: RelayItem.Country = mockk() val mockLocation: GeographicLocationConstraint.Country = mockk(relaxed = true) @@ -158,7 +160,7 @@ class SelectLocationViewModelTest { } @Test - fun testFilterRelay() = runTest { + fun `on onSearchTermInput call uiState should emit with filtered countries`() = runTest { // Arrange val mockCustomList = listOf<RelayItem.CustomList>(mockk()) val mockCountries = listOf<RelayItem.Country>(mockk(), mockk()) @@ -193,7 +195,7 @@ class SelectLocationViewModelTest { } @Test - fun testFilterNotFound() = runTest { + fun `when onSearchTermInput returns empty result uiState should return empty list`() = runTest { // Arrange val mockCustomList = listOf<RelayItem.CustomList>(mockk()) val mockCountries = emptyList<RelayItem.Country>() @@ -220,7 +222,7 @@ class SelectLocationViewModelTest { } @Test - fun testRemoveOwnerFilter() = runTest { + fun `removeOwnerFilter should invoke use case with Constraint Any Ownership`() = runTest { // Arrange val mockSelectedProviders: Constraint<Providers> = mockk() every { mockRelayListFilterUseCase.selectedProviders() } returns @@ -238,7 +240,7 @@ class SelectLocationViewModelTest { } @Test - fun testRemoveProviderFilter() = runTest { + fun `removeProviderFilter should invoke use case with Constraint Any Provider`() = runTest { // Arrange val mockSelectedOwnership: Constraint<Ownership> = mockk() every { mockRelayListFilterUseCase.selectedOwnership() } returns diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt index b52ab53ef6..0eace9ca43 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt @@ -47,7 +47,7 @@ class SettingsViewModelTest { private lateinit var viewModel: SettingsViewModel @BeforeEach - fun setUp() { + fun setup() { mockkStatic(CACHE_EXTENSION_CLASS) val deviceState = MutableStateFlow<DeviceState>(DeviceState.LoggedOut) mockAppVersionInfoCache = @@ -75,72 +75,75 @@ class SettingsViewModelTest { } @Test - fun test_device_state_default_state() = runTest { + fun `uiState should return isLoggedIn false by default`() = runTest { // Act, Assert viewModel.uiState.test { assertEquals(false, awaitItem().isLoggedIn) } } @Test - fun test_device_state_supported_version_state() = runTest { - // Arrange - val versionInfoTestItem = - VersionInfo( - currentVersion = "1.0", - upgradeVersion = "1.0", - isOutdated = false, - isSupported = true - ) - every { mockAppVersionInfoCache.version } returns "1.0" - every { mockAppVersionInfoCache.isSupported } returns true - every { mockAppVersionInfoCache.isOutdated } returns false + fun `when AppVersionInfoCache returns isOutdated false uiState should return isUpdateAvailable false`() = + runTest { + // Arrange + val versionInfoTestItem = + VersionInfo( + currentVersion = "1.0", + upgradeVersion = "1.0", + isOutdated = false, + isSupported = true + ) + every { mockAppVersionInfoCache.version } returns "1.0" + every { mockAppVersionInfoCache.isSupported } returns true + every { mockAppVersionInfoCache.isOutdated } returns false - // Act, Assert - viewModel.uiState.test { - awaitItem() // Wait for initial value + // Act, Assert + viewModel.uiState.test { + awaitItem() // Wait for initial value - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - versionInfo.value = versionInfoTestItem - val result = awaitItem() - assertEquals(false, result.isUpdateAvailable) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + versionInfo.value = versionInfoTestItem + val result = awaitItem() + assertEquals(false, result.isUpdateAvailable) + } } - } @Test - fun test_device_state_unsupported_version_state() = runTest { - // Arrange - every { mockAppVersionInfoCache.isSupported } returns false - every { mockAppVersionInfoCache.isOutdated } returns false - every { mockAppVersionInfoCache.version } returns "" + fun `when AppVersionInfoCache returns isSupported false uiState should return isUpdateAvailable true`() = + runTest { + // Arrange + every { mockAppVersionInfoCache.isSupported } returns false + every { mockAppVersionInfoCache.isOutdated } returns false + every { mockAppVersionInfoCache.version } returns "" - // Act, Assert - viewModel.uiState.test { - awaitItem() + // Act, Assert + viewModel.uiState.test { + awaitItem() - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - val result = awaitItem() - assertEquals(true, result.isUpdateAvailable) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + val result = awaitItem() + assertEquals(true, result.isUpdateAvailable) + } } - } @Test - fun test_device_state_outdated_version_state() = runTest { - // Arrange - every { mockAppVersionInfoCache.isSupported } returns true - every { mockAppVersionInfoCache.isOutdated } returns true - every { mockAppVersionInfoCache.version } returns "" + fun `when AppVersionInfoCache returns isOutdated true uiState should return isUpdateAvailable true`() = + runTest { + // Arrange + every { mockAppVersionInfoCache.isSupported } returns true + every { mockAppVersionInfoCache.isOutdated } returns true + every { mockAppVersionInfoCache.version } returns "" - // Act, Assert - viewModel.uiState.test { - awaitItem() + // Act, Assert + viewModel.uiState.test { + awaitItem() - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - val result = awaitItem() - assertEquals(true, result.isUpdateAvailable) + serviceConnectionState.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + val result = awaitItem() + assertEquals(true, result.isUpdateAvailable) + } } - } 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/SplitTunnelingViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt index c0d349e1da..11b253e5ea 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt @@ -53,7 +53,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_has_progress_on_start() = runTest { + fun `initial state should be loading`() = runTest { initTestSubject(emptyList()) val actualState: SplitTunnelingUiState = testSubject.uiState.value @@ -65,7 +65,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_empty_app_list() = runTest { + fun `empty app list should work`() = runTest { every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers { lambda<(Set<String>) -> Unit>().invoke(emptySet()) @@ -86,7 +86,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_apps_list_delivered() = runTest { + fun `includedApps and excludedApps should both be included in uiState`() = runTest { val appExcluded = AppData("test.excluded", 0, "testName1") val appNotExcluded = AppData("test.not.excluded", 0, "testName2") every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers @@ -119,7 +119,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_include_app() = runTest { + fun `include app should work`() = runTest { var excludedAppsCallback = slot<(Set<String>) -> Unit>() val app = AppData("test", 0, "testName") every { mockedSplitTunneling.includeApp(app.packageName) } just runs @@ -165,7 +165,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_add_app_to_excluded() = runTest { + fun `onExcludeApp should result in new uiState with app excluded`() = runTest { var excludedAppsCallback = slot<(Set<String>) -> Unit>() val app = AppData("test", 0, "testName") every { mockedSplitTunneling.excludeApp(app.packageName) } just runs @@ -212,7 +212,7 @@ class SplitTunnelingViewModelTest { } @Test - fun test_disabled_state() = runTest { + fun `when split tunneling is disabled uiState should be disabled`() = runTest { every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers { lambda<(Set<String>) -> Unit>().invoke(emptySet()) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt index 7c54cd2c5e..6934384643 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt @@ -42,7 +42,7 @@ class VoucherDialogViewModelTest { private lateinit var viewModel: VoucherDialogViewModel @BeforeEach - fun setUp() { + fun setup() { every { mockServiceConnectionManager.connectionState } returns serviceConnectionState viewModel = @@ -58,9 +58,8 @@ class VoucherDialogViewModelTest { } @Test - fun testSubmitVoucher() = runTest { + fun `ensure onRedeem invokes submit on VoucherRedeemer with same voucher code`() = runTest { val voucher = DUMMY_INVALID_VOUCHER - val dummyStringResource = DUMMY_STRING_RESOURCE // Arrange every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer @@ -69,7 +68,7 @@ class VoucherDialogViewModelTest { VoucherSubmissionResult.Ok(mockVoucherSubmission) // Act - assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherViewModelState) + assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherState) viewModel.onRedeem(voucher) // Assert @@ -77,7 +76,7 @@ class VoucherDialogViewModelTest { } @Test - fun testInsertInvalidVoucher() = runTest { + fun `given invalid voucher when redeeming then show error`() = runTest { val voucher = DUMMY_INVALID_VOUCHER val dummyStringResource = DUMMY_STRING_RESOURCE @@ -94,13 +93,13 @@ class VoucherDialogViewModelTest { serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) viewModel.onRedeem(voucher) - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Verifying } - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Error } + assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying } + assertTrue { awaitItem().voucherState is VoucherDialogState.Error } } } @Test - fun testInsertValidVoucher() = runTest { + fun `given valid voucher when redeeming then show success`() = runTest { val voucher = DUMMY_VALID_VOUCHER val dummyStringResource = DUMMY_STRING_RESOURCE @@ -117,13 +116,13 @@ class VoucherDialogViewModelTest { serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) viewModel.onRedeem(voucher) - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Verifying } - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Success } + assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying } + assertTrue { awaitItem().voucherState is VoucherDialogState.Success } } } @Test - fun testResetStateAfterChangingInput() = runTest { + fun `when voucher input is changed then clear error`() = runTest { val voucher = DUMMY_INVALID_VOUCHER val dummyStringResource = DUMMY_STRING_RESOURCE @@ -140,10 +139,10 @@ class VoucherDialogViewModelTest { serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) viewModel.onRedeem(voucher) - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Verifying } - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Error } + assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying } + assertTrue { awaitItem().voucherState is VoucherDialogState.Error } viewModel.onVoucherInputChange(DUMMY_VALID_VOUCHER) - assertTrue { awaitItem().voucherViewModelState is VoucherDialogState.Default } + assertTrue { awaitItem().voucherState is VoucherDialogState.Default } } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt index 5d44dca487..11992c40c0 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt @@ -48,7 +48,7 @@ class VpnSettingsViewModelTest { private lateinit var viewModel: VpnSettingsViewModel @BeforeEach - fun setUp() { + fun setup() { every { mockSettingsRepository.settingsUpdates } returns mockSettingsUpdate every { mockPortRangeUseCase.portRanges() } returns portRangeFlow @@ -70,18 +70,20 @@ class VpnSettingsViewModelTest { } @Test - fun test_select_quantum_resistant_state_select() = runTest { - val quantumResistantState = QuantumResistantState.On - every { mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState) } returns - Unit - viewModel.onSelectQuantumResistanceSetting(quantumResistantState) - verify(exactly = 1) { - mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState) + fun `onSelectQuantumResistanceSetting should invoke setWireguardQuantumResistant on SettingsRepository`() = + runTest { + val quantumResistantState = QuantumResistantState.On + every { + mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState) + } returns Unit + viewModel.onSelectQuantumResistanceSetting(quantumResistantState) + verify(exactly = 1) { + mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState) + } } - } @Test - fun test_update_quantum_resistant_default_state() = runTest { + fun `quantumResistant should be Off in uiState in initial state`() = runTest { // Arrange val expectedResistantState = QuantumResistantState.Off @@ -92,66 +94,69 @@ class VpnSettingsViewModelTest { } @Test - fun test_update_quantum_resistant_update_state() = runTest { - val defaultResistantState = QuantumResistantState.Off - val expectedResistantState = QuantumResistantState.On - val mockSettings: Settings = mockk(relaxed = true) - val mockTunnelOptions: TunnelOptions = mockk(relaxed = true) - val mockWireguardTunnelOptions: WireguardTunnelOptions = mockk(relaxed = true) + fun `when SettingsRepository emits quantumResistant On uiState should emit quantumResistant On`() = + runTest { + val defaultResistantState = QuantumResistantState.Off + val expectedResistantState = QuantumResistantState.On + val mockSettings: Settings = mockk(relaxed = true) + val mockTunnelOptions: TunnelOptions = mockk(relaxed = true) + val mockWireguardTunnelOptions: WireguardTunnelOptions = mockk(relaxed = true) - every { mockSettings.tunnelOptions } returns mockTunnelOptions - every { mockTunnelOptions.wireguard } returns mockWireguardTunnelOptions - every { mockWireguardTunnelOptions.quantumResistant } returns expectedResistantState - every { mockSettings.relaySettings } returns mockk<RelaySettings.Normal>(relaxed = true) + every { mockSettings.tunnelOptions } returns mockTunnelOptions + every { mockTunnelOptions.wireguard } returns mockWireguardTunnelOptions + every { mockWireguardTunnelOptions.quantumResistant } returns expectedResistantState + every { mockSettings.relaySettings } returns mockk<RelaySettings.Normal>(relaxed = true) - viewModel.uiState.test { - assertEquals(defaultResistantState, awaitItem().quantumResistant) - mockSettingsUpdate.value = mockSettings - assertEquals(expectedResistantState, awaitItem().quantumResistant) + viewModel.uiState.test { + assertEquals(defaultResistantState, awaitItem().quantumResistant) + mockSettingsUpdate.value = mockSettings + assertEquals(expectedResistantState, awaitItem().quantumResistant) + } } - } @Test - fun test_update_wireguard_port_state() = runTest { - // Arrange - val expectedPort: Constraint<Port> = Constraint.Only(Port(99)) - val mockSettings: Settings = mockk(relaxed = true) - val mockRelaySettings: RelaySettings.Normal = mockk() - val mockRelayConstraints: RelayConstraints = mockk() - val mockWireguardConstraints: WireguardConstraints = mockk() + fun `when SettingsRepository emits Constraint Only then uiState should emit custom and selectedWireguardPort with port of Constraint`() = + runTest { + // Arrange + val expectedPort: Constraint<Port> = Constraint.Only(Port(99)) + val mockSettings: Settings = mockk(relaxed = true) + val mockRelaySettings: RelaySettings.Normal = mockk() + val mockRelayConstraints: RelayConstraints = mockk() + val mockWireguardConstraints: WireguardConstraints = mockk() - every { mockSettings.relaySettings } returns mockRelaySettings - every { mockRelaySettings.relayConstraints } returns mockRelayConstraints - every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints - every { mockWireguardConstraints.port } returns expectedPort + every { mockSettings.relaySettings } returns mockRelaySettings + every { mockRelaySettings.relayConstraints } returns mockRelayConstraints + every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints + every { mockWireguardConstraints.port } returns expectedPort - // Act, Assert - viewModel.uiState.test { - assertIs<Constraint.Any<Port>>(awaitItem().selectedWireguardPort) - mockSettingsUpdate.value = mockSettings - assertEquals(expectedPort, awaitItem().customWireguardPort) - assertEquals(expectedPort, awaitItem().selectedWireguardPort) + // Act, Assert + viewModel.uiState.test { + assertIs<Constraint.Any<Port>>(awaitItem().selectedWireguardPort) + mockSettingsUpdate.value = mockSettings + assertEquals(expectedPort, awaitItem().customWireguardPort) + assertEquals(expectedPort, awaitItem().selectedWireguardPort) + } } - } @Test - fun test_select_wireguard_port() = runTest { - // Arrange - val wireguardPort: Constraint<Port> = Constraint.Only(Port(99)) - val wireguardConstraints = WireguardConstraints(port = wireguardPort) - every { mockRelayListUseCase.updateSelectedWireguardConstraints(any()) } returns Unit + fun `onWireguardPortSelected should invoke updateSelectedWireguardConstraint with Constraint Only with same port`() = + runTest { + // Arrange + val wireguardPort: Constraint<Port> = Constraint.Only(Port(99)) + val wireguardConstraints = WireguardConstraints(port = wireguardPort) + every { mockRelayListUseCase.updateSelectedWireguardConstraints(any()) } returns Unit - // Act - viewModel.onWireguardPortSelected(wireguardPort) + // Act + viewModel.onWireguardPortSelected(wireguardPort) - // Assert - verify(exactly = 1) { - mockRelayListUseCase.updateSelectedWireguardConstraints(wireguardConstraints) + // Assert + verify(exactly = 1) { + mockRelayListUseCase.updateSelectedWireguardConstraints(wireguardConstraints) + } } - } @Test - fun `given useCase systemVpnSettingsAvailable is true then uiState should be systemVpnSettingsAvailable=true`() = + fun `when useCase systemVpnSettingsAvailable is true then uiState should be systemVpnSettingsAvailable=true`() = runTest { val systemVpnSettingsAvailable = true 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 d0461a4ee5..74ea210a60 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 @@ -69,7 +69,7 @@ class WelcomeViewModelTest { private lateinit var viewModel: WelcomeViewModel @BeforeEach - fun setUp() { + fun setup() { mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS) mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS) @@ -108,7 +108,7 @@ class WelcomeViewModelTest { } @Test - fun testSitePaymentClick() = runTest { + fun `on onSitePaymentClick call uiSideEffect should emit OpenAccountView`() = runTest { // Arrange val mockToken = "4444 5555 6666 7777" val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true) @@ -125,9 +125,9 @@ class WelcomeViewModelTest { } @Test - fun testUpdateTunnelState() = runTest { + fun `on new TunnelState uiState should include new TunnelState`() = runTest { // Arrange - val tunnelUiStateTestItem = TunnelState.Connected(mockk(), mockk()) + val tunnelUiStateTestItem: TunnelState = mockk() // Act, Assert viewModel.uiState.test { @@ -141,105 +141,110 @@ class WelcomeViewModelTest { } @Test - fun testUpdateAccountNumber() = runTest { - // Arrange - val expectedAccountNumber = "4444555566667777" - val device: Device = mockk() - every { device.displayName() } returns "" + fun `when DeviceRepository returns LoggedIn uiState should include new accountNumber`() = + runTest { + // Arrange + val expectedAccountNumber = "4444555566667777" + val device: Device = mockk() + every { device.displayName() } returns "" - // Act, Assert - viewModel.uiState.test { - assertEquals(WelcomeUiState(), awaitItem()) - paymentAvailabilityFlow.value = null - deviceStateFlow.value = - DeviceState.LoggedIn( - accountAndDevice = - AccountAndDevice(account_token = expectedAccountNumber, device = device) - ) - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - assertEquals(expectedAccountNumber, awaitItem().accountNumber) + // Act, Assert + viewModel.uiState.test { + assertEquals(WelcomeUiState(), awaitItem()) + paymentAvailabilityFlow.value = null + deviceStateFlow.value = + DeviceState.LoggedIn( + accountAndDevice = + AccountAndDevice(account_token = expectedAccountNumber, device = device) + ) + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + assertEquals(expectedAccountNumber, awaitItem().accountNumber) + } } - } @Test - fun testOpenConnectScreen() = runTest { - // Arrange - val mockExpiryDate: DateTime = mockk() - every { mockExpiryDate.isAfter(any<ReadableInstant>()) } returns true + fun `when OutOfTimeUseCase return false uiSideEffect should emit OpenConnectScreen`() = + runTest { + // Arrange + val mockExpiryDate: DateTime = mockk() + every { mockExpiryDate.isAfter(any<ReadableInstant>()) } returns true - // Act, Assert - viewModel.uiSideEffect.test { - outOfTimeFlow.value = false - val action = awaitItem() - assertIs<WelcomeViewModel.UiSideEffect.OpenConnectScreen>(action) + // Act, Assert + viewModel.uiSideEffect.test { + outOfTimeFlow.value = false + val action = awaitItem() + assertIs<WelcomeViewModel.UiSideEffect.OpenConnectScreen>(action) + } } - } @Test - fun testBillingProductsUnavailableState() = runTest { - // Arrange - val productsUnavailable = PaymentAvailability.ProductsUnavailable + fun `when paymentAvailability emits ProductsUnavailable uiState should include state NoPayment`() = + runTest { + // Arrange + val productsUnavailable = PaymentAvailability.ProductsUnavailable - // Act, Assert - viewModel.uiState.test { - // Default item - awaitItem() - paymentAvailabilityFlow.tryEmit(productsUnavailable) - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - val result = awaitItem().billingPaymentState - assertIs<PaymentState.NoPayment>(result) + // Act, Assert + viewModel.uiState.test { + // Default item + awaitItem() + paymentAvailabilityFlow.tryEmit(productsUnavailable) + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + val result = awaitItem().billingPaymentState + assertIs<PaymentState.NoPayment>(result) + } } - } @Test - fun testBillingProductsGenericErrorState() = runTest { - // Arrange - val paymentOtherError = PaymentAvailability.Error.Other(mockk()) - paymentAvailabilityFlow.tryEmit(paymentOtherError) - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ErrorOther uiState should include state ErrorGeneric`() = + runTest { + // Arrange + val paymentOtherError = PaymentAvailability.Error.Other(mockk()) + paymentAvailabilityFlow.tryEmit(paymentOtherError) + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.Error.Generic>(result) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.Error.Generic>(result) + } } - } @Test - fun testBillingProductsBillingErrorState() = runTest { - // Arrange - val paymentBillingError = PaymentAvailability.Error.BillingUnavailable - paymentAvailabilityFlow.value = paymentBillingError - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ErrorBillingUnavailable uiState should include state ErrorBilling`() = + runTest { // Arrange + val paymentBillingError = PaymentAvailability.Error.BillingUnavailable + paymentAvailabilityFlow.value = paymentBillingError + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.Error.Billing>(result) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.Error.Billing>(result) + } } - } @Test - fun testBillingProductsPaymentAvailableState() = runTest { - // Arrange - val mockProduct: PaymentProduct = mockk() - val expectedProductList = listOf(mockProduct) - val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct)) - paymentAvailabilityFlow.value = productsAvailable - serviceConnectionStateFlow.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) + fun `when paymentAvailability emits ProductsAvailable uiState should include state Available with products`() = + runTest { + // Arrange + val mockProduct: PaymentProduct = mockk() + val expectedProductList = listOf(mockProduct) + val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct)) + paymentAvailabilityFlow.value = productsAvailable + serviceConnectionStateFlow.value = + ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - // Act, Assert - viewModel.uiState.test { - val result = awaitItem().billingPaymentState - assertIs<PaymentState.PaymentAvailable>(result) - assertLists(expectedProductList, result.products) + // Act, Assert + viewModel.uiState.test { + val result = awaitItem().billingPaymentState + assertIs<PaymentState.PaymentAvailable>(result) + assertLists(expectedProductList, result.products) + } } - } companion object { private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS = diff --git a/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt b/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt index 386932681d..7ca3219cfb 100644 --- a/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt +++ b/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt @@ -52,7 +52,7 @@ class BillingRepositoryTest { CapturingSlot() @BeforeEach - fun setUp() { + fun setup() { mockkStatic(BILLING_CLIENT_CLASS) mockkStatic(BILLING_CLIENT_KOTLIN_CLASS) mockkStatic(BILLING_FLOW_PARAMS) diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt index 37cc701724..2cb3e1371a 100644 --- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt @@ -17,7 +17,7 @@ fun ProductDetailsResult.toPaymentAvailability( productDetailsList.toPaymentProducts(productIdToPaymentStatus) ) } else { - PaymentAvailability.NoProductsFounds + PaymentAvailability.NoProductsFound } } BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt index 21e13d5da8..c4d1b04905 100644 --- a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt +++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt @@ -40,7 +40,7 @@ class BillingPaymentRepositoryTest { private lateinit var paymentRepository: BillingPaymentRepository @BeforeEach - fun setUp() { + fun setup() { mockkStatic(PRODUCT_DETAILS_TO_PAYMENT_PRODUCT_EXT) every { mockBillingRepository.purchaseEvents } returns purchaseEventFlow @@ -53,7 +53,7 @@ class BillingPaymentRepositoryTest { } @Test - fun testQueryAvailablePaymentProductsAvailable() = runTest { + fun `queryPaymentAvailability should return available products when billing is OK`() = runTest { // Arrange val expectedProduct: PaymentProduct = mockk() val mockProduct: ProductDetails = mockk() @@ -76,45 +76,47 @@ class BillingPaymentRepositoryTest { } @Test - fun testQueryAvailablePaymentProductsUnavailable() = runTest { - // Arrange - val mockResult: ProductDetailsResult = mockk() - every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockResult.productDetailsList } returns emptyList() - coEvery { mockBillingRepository.queryPurchases() } returns mockk(relaxed = true) - coEvery { mockBillingRepository.queryProducts(any()) } returns mockResult + fun `queryPaymentAvailability should return NoProductsFound when billing is OK with no products `() = + runTest { + // Arrange + val mockResult: ProductDetailsResult = mockk() + every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK + every { mockResult.productDetailsList } returns emptyList() + coEvery { mockBillingRepository.queryPurchases() } returns mockk(relaxed = true) + coEvery { mockBillingRepository.queryProducts(any()) } returns mockResult - // Act, Assert - paymentRepository.queryPaymentAvailability().test { - // Loading - awaitItem() - val result = awaitItem() - assertIs<PaymentAvailability.NoProductsFounds>(result) - awaitComplete() + // Act, Assert + paymentRepository.queryPaymentAvailability().test { + // Loading + awaitItem() + val result = awaitItem() + assertIs<PaymentAvailability.NoProductsFound>(result) + awaitComplete() + } } - } @Test - fun testQueryAvailablePaymentBillingUnavailableError() = runTest { - // Arrange - val mockResult: ProductDetailsResult = mockk() - every { mockResult.billingResult.responseCode } returns - BillingResponseCode.BILLING_UNAVAILABLE - coEvery { mockBillingRepository.queryPurchases() } returns mockk(relaxed = true) - coEvery { mockBillingRepository.queryProducts(any()) } returns mockResult + fun `queryPaymentAvailability should return BillingUnavailable when billing is Unavailable `() = + runTest { + // Arrange + val mockResult: ProductDetailsResult = mockk() + every { mockResult.billingResult.responseCode } returns + BillingResponseCode.BILLING_UNAVAILABLE + coEvery { mockBillingRepository.queryPurchases() } returns mockk(relaxed = true) + coEvery { mockBillingRepository.queryProducts(any()) } returns mockResult - // Act, Assert - paymentRepository.queryPaymentAvailability().test { - // Loading - awaitItem() - val result = awaitItem() - assertIs<PaymentAvailability.Error.BillingUnavailable>(result) - awaitComplete() + // Act, Assert + paymentRepository.queryPaymentAvailability().test { + // Loading + awaitItem() + val result = awaitItem() + assertIs<PaymentAvailability.Error.BillingUnavailable>(result) + awaitComplete() + } } - } @Test - fun testPurchaseBillingProductStartPurchaseFetchProductsError() = runTest { + fun `purchaseProduct should return FetchProductsError when billing is Unavailable`() = runTest { // Arrange val mockProductId = ProductId("MOCK") val mockProductDetailsResult = mockk<ProductDetailsResult>() @@ -134,85 +136,91 @@ class BillingPaymentRepositoryTest { } @Test - fun testPurchaseBillingProductStartPurchaseNoProductsFoundError() = runTest { - // Arrange - val mockProductId = ProductId("MOCK") - val mockProductDetailsResult = mockk<ProductDetailsResult>() - every { mockProductDetailsResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockProductDetailsResult.productDetailsList } returns emptyList() - coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns - mockProductDetailsResult + fun `purchaseProduct should return NoProductFound when billingRepository does not find any products`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk<ProductDetailsResult>() + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns emptyList() + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult - // Act, Assert - paymentRepository.purchaseProduct(mockProductId, mockk()).test { - assertIs<PurchaseResult.FetchingProducts>(awaitItem()) - val result = awaitItem() - assertIs<PurchaseResult.Error.NoProductFound>(result) - awaitComplete() + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + assertIs<PurchaseResult.FetchingProducts>(awaitItem()) + val result = awaitItem() + assertIs<PurchaseResult.Error.NoProductFound>(result) + awaitComplete() + } } - } @Test - fun testPurchaseBillingProductStartPurchaseTransactionIdError() = runTest { - // Arrange - val mockProductId = ProductId("MOCK") - val mockProductDetailsResult = mockk<ProductDetailsResult>() - val mockProductDetails: ProductDetails = mockk() - every { mockProductDetails.productId } returns mockProductId.value - every { mockProductDetailsResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) - coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns - mockProductDetailsResult - coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError) + fun `purchaseProduct should return TransactionIdError on PlayPurchaseInitError from PlayPurchaseRepository`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk<ProductDetailsResult>() + val mockProductDetails: ProductDetails = mockk() + every { mockProductDetails.productId } returns mockProductId.value + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult + coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns + PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError) - // Act, Assert - paymentRepository.purchaseProduct(mockProductId, mockk()).test { - assertIs<PurchaseResult.FetchingProducts>(awaitItem()) - assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) - val result = awaitItem() - assertIs<PurchaseResult.Error.TransactionIdError>(result) - awaitComplete() + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + assertIs<PurchaseResult.FetchingProducts>(awaitItem()) + assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) + val result = awaitItem() + assertIs<PurchaseResult.Error.TransactionIdError>(result) + awaitComplete() + } } - } @Test - fun testPurchaseBillingProductStartPurchaseFlowBillingError() = runTest { - // Arrange - val mockProductId = ProductId("MOCK") - val mockProductDetailsResult = mockk<ProductDetailsResult>() - val mockProductDetails: ProductDetails = mockk() - every { mockProductDetails.productId } returns mockProductId.value - every { mockProductDetailsResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) - coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns - mockProductDetailsResult - val mockBillingResult: BillingResult = mockk() - every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE - every { mockBillingResult.debugMessage } returns "Mock error" - coEvery { - mockBillingRepository.startPurchaseFlow( - productDetails = any(), - obfuscatedId = any(), - activityProvider = any() - ) - } returns mockBillingResult - coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK") + fun `purchaseProduct should return BillingError on billing unavailable from startPurchaseFlow`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk<ProductDetailsResult>() + val mockProductDetails: ProductDetails = mockk() + every { mockProductDetails.productId } returns mockProductId.value + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult + val mockBillingResult: BillingResult = mockk() + every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE + every { mockBillingResult.debugMessage } returns "Mock error" + coEvery { + mockBillingRepository.startPurchaseFlow( + productDetails = any(), + obfuscatedId = any(), + activityProvider = any() + ) + } returns mockBillingResult + coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns + PlayPurchaseInitResult.Ok("MOCK") - // Act, Assert - paymentRepository.purchaseProduct(mockProductId, mockk()).test { - // Purchase started - assertIs<PurchaseResult.FetchingProducts>(awaitItem()) - assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) - val result = awaitItem() - assertIs<PurchaseResult.Error.BillingError>(result) - awaitComplete() + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + // Purchase started + assertIs<PurchaseResult.FetchingProducts>(awaitItem()) + assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) + val result = awaitItem() + assertIs<PurchaseResult.Error.BillingError>(result) + awaitComplete() + } } - } @Test - fun testPurchaseBillingProductPurchaseCanceled() = runTest { + fun `cancel during purchaseProduct should return in cancelled event`() = runTest { // Arrange val mockProductId = ProductId("MOCK") val mockProductDetailsResult = mockk<ProductDetailsResult>() @@ -248,50 +256,52 @@ class BillingPaymentRepositoryTest { } @Test - fun testPurchaseBillingProductVerificationError() = runTest { - // Arrange - val mockProductId = ProductId("MOCK") - val mockProductDetailsResult = mockk<ProductDetailsResult>() - val mockProductDetails: ProductDetails = mockk() - every { mockProductDetails.productId } returns mockProductId.value - every { mockProductDetailsResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) - coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns - mockProductDetailsResult - val mockPurchaseToken = "TOKEN" - val mockBillingPurchase: Purchase = mockk() - val mockBillingResult: BillingResult = mockk() - every { mockBillingPurchase.purchaseState } returns Purchase.PurchaseState.PURCHASED - every { mockBillingResult.responseCode } returns BillingResponseCode.OK - every { mockBillingPurchase.products } returns listOf(mockProductId.value) - every { mockBillingPurchase.purchaseToken } returns mockPurchaseToken - coEvery { - mockBillingRepository.startPurchaseFlow( - productDetails = any(), - obfuscatedId = any(), - activityProvider = any() - ) - } returns mockBillingResult - coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK-ID") - coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns - PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError) + fun `purchaseProduct should emit VerificationError on verify error from playPurchase`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk<ProductDetailsResult>() + val mockProductDetails: ProductDetails = mockk() + every { mockProductDetails.productId } returns mockProductId.value + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult + val mockPurchaseToken = "TOKEN" + val mockBillingPurchase: Purchase = mockk() + val mockBillingResult: BillingResult = mockk() + every { mockBillingPurchase.purchaseState } returns Purchase.PurchaseState.PURCHASED + every { mockBillingResult.responseCode } returns BillingResponseCode.OK + every { mockBillingPurchase.products } returns listOf(mockProductId.value) + every { mockBillingPurchase.purchaseToken } returns mockPurchaseToken + coEvery { + mockBillingRepository.startPurchaseFlow( + productDetails = any(), + obfuscatedId = any(), + activityProvider = any() + ) + } returns mockBillingResult + coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns + PlayPurchaseInitResult.Ok("MOCK-ID") + coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns + PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError) - // Act, Assert - paymentRepository.purchaseProduct(mockProductId, mockk()).test { - assertIs<PurchaseResult.FetchingProducts>(awaitItem()) - assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) - assertIs<PurchaseResult.BillingFlowStarted>(awaitItem()) - purchaseEventFlow.tryEmit(PurchaseEvent.Completed(listOf(mockBillingPurchase))) - assertIs<PurchaseResult.VerificationStarted>(awaitItem()) - val result = awaitItem() - assertIs<PurchaseResult.Error.VerificationError>(result) - awaitComplete() + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + assertIs<PurchaseResult.FetchingProducts>(awaitItem()) + assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) + assertIs<PurchaseResult.BillingFlowStarted>(awaitItem()) + purchaseEventFlow.tryEmit(PurchaseEvent.Completed(listOf(mockBillingPurchase))) + assertIs<PurchaseResult.VerificationStarted>(awaitItem()) + val result = awaitItem() + assertIs<PurchaseResult.Error.VerificationError>(result) + awaitComplete() + } } - } @Test - fun testPurchaseBillingProductPurchaseCompleted() = runTest { + fun `purchaseProduct success should emit all events leading up to success`() = runTest { // Arrange val mockProductId = ProductId("MOCK") val mockProductDetailsResult = mockk<ProductDetailsResult>() @@ -334,41 +344,43 @@ class BillingPaymentRepositoryTest { } @Test - fun testPurchaseBillingProductPurchasePending() = runTest { - // Arrange - val mockProductId = ProductId("MOCK") - val mockProductDetailsResult = mockk<ProductDetailsResult>() - val mockProductDetails: ProductDetails = mockk() - every { mockProductDetails.productId } returns mockProductId.value - every { mockProductDetailsResult.billingResult.responseCode } returns BillingResponseCode.OK - every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) - coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns - mockProductDetailsResult - val mockBillingPurchase: Purchase = mockk() - val mockBillingResult: BillingResult = mockk() - every { mockBillingPurchase.purchaseState } returns Purchase.PurchaseState.PENDING - every { mockBillingResult.responseCode } returns BillingResponseCode.OK - coEvery { - mockBillingRepository.startPurchaseFlow( - productDetails = any(), - obfuscatedId = any(), - activityProvider = any() - ) - } returns mockBillingResult - coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK") + fun `purchaseProduct where purchase gets stuck in pending should complete with pending`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk<ProductDetailsResult>() + val mockProductDetails: ProductDetails = mockk() + every { mockProductDetails.productId } returns mockProductId.value + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult + val mockBillingPurchase: Purchase = mockk() + val mockBillingResult: BillingResult = mockk() + every { mockBillingPurchase.purchaseState } returns Purchase.PurchaseState.PENDING + every { mockBillingResult.responseCode } returns BillingResponseCode.OK + coEvery { + mockBillingRepository.startPurchaseFlow( + productDetails = any(), + obfuscatedId = any(), + activityProvider = any() + ) + } returns mockBillingResult + coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns + PlayPurchaseInitResult.Ok("MOCK") - // Act, Assert - paymentRepository.purchaseProduct(mockProductId, mockk()).test { - assertIs<PurchaseResult.FetchingProducts>(awaitItem()) - assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) - assertIs<PurchaseResult.BillingFlowStarted>(awaitItem()) - purchaseEventFlow.tryEmit(PurchaseEvent.Completed(listOf(mockBillingPurchase))) - val result = awaitItem() - assertIs<PurchaseResult.Completed.Pending>(result) - awaitComplete() + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + assertIs<PurchaseResult.FetchingProducts>(awaitItem()) + assertIs<PurchaseResult.FetchingObfuscationId>(awaitItem()) + assertIs<PurchaseResult.BillingFlowStarted>(awaitItem()) + purchaseEventFlow.tryEmit(PurchaseEvent.Completed(listOf(mockBillingPurchase))) + val result = awaitItem() + assertIs<PurchaseResult.Completed.Pending>(result) + awaitComplete() + } } - } companion object { private const val PRODUCT_DETAILS_TO_PAYMENT_PRODUCT_EXT = diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt index 8788c2123a..c883f20bfc 100644 --- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt @@ -140,7 +140,7 @@ class LatitudeTest { } @Test - fun `distanceTo with two positive latitudes`() { + fun `distanceTo with two positive latitudes should return distance`() { val latFloat1 = 80f val latitude1 = Latitude(latFloat1) val latFloat2 = 30f @@ -150,7 +150,7 @@ class LatitudeTest { } @Test - fun `distanceTo with two negative latitudes`() { + fun `distanceTo with two negative latitudes should return distance`() { val latFloat1 = -80f val latitude1 = Latitude(latFloat1) val latFloat2 = -30f diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt index de94661ad0..69d3445417 100644 --- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt @@ -119,7 +119,7 @@ class LongitudeTest { } @Test - fun `distanceTo with two positive longitudes`() { + fun `distanceTo with two positive longitudes should return distance`() { val longFloat1 = 80f val longitude1 = Longitude(longFloat1) val longFloat2 = 30f @@ -129,7 +129,7 @@ class LongitudeTest { } @Test - fun `distanceTo with two negative longitudes`() { + fun `distanceTo with two negative longitudes should return distance`() { val longFloat1 = -80f val longitude1 = Longitude(longFloat1) val longFloat2 = -30f @@ -141,7 +141,7 @@ class LongitudeTest { } @Test - fun `distanceTo with wrapping value as shortest path`() { + fun `distanceTo with wrapping value should return shortest path as distance`() { val longFloat1 = -170f val longitude1 = Longitude(longFloat1) val longFloat2 = 170f diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt index 012237d825..43caa9d7e2 100644 --- a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt +++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt @@ -7,7 +7,7 @@ sealed interface PaymentAvailability { data object ProductsUnavailable : PaymentAvailability - data object NoProductsFounds : PaymentAvailability + data object NoProductsFound : PaymentAvailability sealed interface Error : PaymentAvailability { data object BillingUnavailable : Error diff --git a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt index e676b55d7d..6a5b874340 100644 --- a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt +++ b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/JUnitTest.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.test.arch import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.verify.assertEmpty +import com.lemonappdev.konsist.api.verify.assertTrue import org.junit.jupiter.api.Test class JUnitTest { @@ -27,4 +28,38 @@ class JUnitTest { } } .assertEmpty() + + @Test + fun `ensure all non android tests are written with spaces`() = + allNonAndroidTests().assertTrue { it.name.contains(' ') } + + @Test + fun `ensure all non android tests does start with lower case letter`() = + allNonAndroidTests().assertTrue { it.name.first().isLowerCase() } + + @Test + fun `ensure all non android tests have 'ensure' or 'should' in function name`() = + allNonAndroidTests().assertTrue { it.name.containsEnsureOrShould() } + + private fun String.containsEnsureOrShould(): Boolean { + return contains("ensure") || contains("should") || contains("then") + } + + private fun allNonAndroidTests() = + Konsist.scopeFromTest() + .functions() + // withAnnotationOf is broken in latest Consist version, so we filter manually + // https://github.com/LemonAppDev/konsist/discussions/738 + .filter { it.annotations.any { it.text == "@Test" } } + .filter { it.sourceSetName != "androidTest" } + .filter { function -> + ignoredTestPackages.none { function.packagee!!.fullyQualifiedName.startsWith(it) } + } + + companion object { + // The following packages are not following the naming convention since they are android + // test that does not support spaces in function names. + private val ignoredTestPackages = + listOf("net.mullvad.mullvadvpn.test.e2e", "net.mullvad.mullvadvpn.test.mockapi") + } } |
